1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-12-20 12:30:56 +01:00

When no master database is configured, automatically degrade to read-only mode

Summary: Ref T4571. If `cluster.databases` is configured but only has replicas, implicitly drop to read-only mode and send writes to a replica.

Test Plan:
  - Disabled the `master`, saw Phabricator automatically degrade into read-only mode against replicas.
  - (Also tested: explicit read-only mode, non-cluster mode, properly configured cluster mode).

Reviewers: chad

Reviewed By: chad

Maniphest Tasks: T4571

Differential Revision: https://secure.phabricator.com/D15672
This commit is contained in:
epriestley 2016-04-10 05:10:06 -07:00
parent 071741c61d
commit e0a8cac703
8 changed files with 146 additions and 15 deletions

View file

@ -1987,6 +1987,9 @@ phutil_register_library_map(array(
'PhabricatorChunkedFileStorageEngine' => 'applications/files/engine/PhabricatorChunkedFileStorageEngine.php',
'PhabricatorClusterConfigOptions' => 'applications/config/option/PhabricatorClusterConfigOptions.php',
'PhabricatorClusterDatabasesConfigOptionType' => 'infrastructure/cluster/PhabricatorClusterDatabasesConfigOptionType.php',
'PhabricatorClusterException' => 'infrastructure/cluster/PhabricatorClusterException.php',
'PhabricatorClusterExceptionHandler' => 'infrastructure/cluster/PhabricatorClusterExceptionHandler.php',
'PhabricatorClusterImproperWriteException' => 'infrastructure/cluster/PhabricatorClusterImproperWriteException.php',
'PhabricatorColumnProxyInterface' => 'applications/project/interface/PhabricatorColumnProxyInterface.php',
'PhabricatorColumnsEditField' => 'applications/transactions/editfield/PhabricatorColumnsEditField.php',
'PhabricatorCommentEditEngineExtension' => 'applications/transactions/engineextension/PhabricatorCommentEditEngineExtension.php',
@ -6397,6 +6400,9 @@ phutil_register_library_map(array(
'PhabricatorChunkedFileStorageEngine' => 'PhabricatorFileStorageEngine',
'PhabricatorClusterConfigOptions' => 'PhabricatorApplicationConfigOptions',
'PhabricatorClusterDatabasesConfigOptionType' => 'PhabricatorConfigJSONOptionType',
'PhabricatorClusterException' => 'Exception',
'PhabricatorClusterExceptionHandler' => 'PhabricatorRequestExceptionHandler',
'PhabricatorClusterImproperWriteException' => 'PhabricatorClusterException',
'PhabricatorColumnsEditField' => 'PhabricatorPHIDListEditField',
'PhabricatorCommentEditEngineExtension' => 'PhabricatorEditEngineExtension',
'PhabricatorCommentEditField' => 'PhabricatorEditField',

View file

@ -25,6 +25,27 @@ final class PhabricatorSystemReadOnlyController
'has been turned on by rolling your chair away from your desk and '.
'yelling "Hey! Why is Phabricator in read-only mode??!" using '.
'your very loudest outside voice.');
$body[] = pht(
'This mode is active because it is enabled in the configuration '.
'option "%s".',
phutil_tag('tt', array(), 'cluster.read-only'));
$button = pht('Wait Patiently');
break;
case PhabricatorEnv::READONLY_MASTERLESS:
$title = pht('No Writable Database');
$body[] = pht(
'Phabricator is currently configured with no writable ("master") '.
'database, so it can not write new information anywhere. '.
'Phabricator will run in read-only mode until an administrator '.
'reconfigures it with a writable database.');
$body[] = pht(
'This usually occurs when an administrator is actively working on '.
'fixing a temporary configuration or deployment problem.');
$body[] = pht(
'This mode is active because no database has a "%s" role in '.
'the configuration option "%s".',
phutil_tag('tt', array(), 'master'),
phutil_tag('tt', array(), 'cluster.databases'));
$button = pht('Wait Patiently');
break;
default:
@ -33,8 +54,8 @@ final class PhabricatorSystemReadOnlyController
$body[] = pht(
'In read-only mode you can read existing information, but you will not '.
'be able to edit information or create new information until this mode '.
'is disabled.');
'be able to edit objects or create new objects until this mode is '.
'disabled.');
$dialog = $this->newDialog()
->setTitle($title)

View file

@ -0,0 +1,8 @@
<?php
abstract class PhabricatorClusterException
extends Exception {
abstract public function getExceptionTitle();
}

View file

@ -0,0 +1,35 @@
<?php
final class PhabricatorClusterExceptionHandler
extends PhabricatorRequestExceptionHandler {
public function getRequestExceptionHandlerPriority() {
return 300000;
}
public function getRequestExceptionHandlerDescription() {
return pht('Handles runtime problems with cluster configuration.');
}
public function canHandleRequestException(
AphrontRequest $request,
Exception $ex) {
return ($ex instanceof PhabricatorClusterException);
}
public function handleRequestException(
AphrontRequest $request,
Exception $ex) {
$viewer = $this->getViewer($request);
$title = $ex->getExceptionTitle();
return id(new AphrontDialogView())
->setTitle($title)
->setUser($viewer)
->appendParagraph($ex->getMessage())
->addCancelButton('/', pht('Proceed With Caution'));
}
}

View file

@ -0,0 +1,10 @@
<?php
final class PhabricatorClusterImproperWriteException
extends PhabricatorClusterException {
public function getExceptionTitle() {
return pht('Improper Cluster Write');
}
}

View file

@ -347,6 +347,31 @@ final class PhabricatorDatabaseRef
return null;
}
public static function getReplicaDatabaseRef() {
$refs = self::loadAll();
if (!$refs) {
return null;
}
// 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.
foreach ($refs as $ref) {
if ($ref->getDisabled()) {
continue;
}
if ($ref->getIsMaster()) {
continue;
}
return $ref;
}
return null;
}
private function newConnection(array $options) {
$spec = $options + array(
'user' => $this->getUser(),

View file

@ -60,6 +60,7 @@ final class PhabricatorEnv extends Phobject {
private static $readOnlyReason;
const READONLY_CONFIG = 'config';
const READONLY_MASTERLESS = 'masterless';
/**
* @phutil-external-symbol class PhabricatorStartup
@ -213,6 +214,11 @@ final class PhabricatorEnv extends Phobject {
$stack->pushSource($site_source);
}
$master = PhabricatorDatabaseRef::getMasterDatabaseRef();
if (!$master) {
self::setReadOnly(true, self::READONLY_MASTERLESS);
}
try {
$stack->pushSource(
id(new PhabricatorConfigDatabaseSource('default'))
@ -456,7 +462,15 @@ final class PhabricatorEnv extends Phobject {
}
public static function getReadOnlyMessage() {
return pht('Phabricator is currently in read-only mode.');
$reason = self::getReadOnlyReason();
switch ($reason) {
case self::READONLY_MASTERLESS:
return pht(
'Phabricator is in read-only mode (no writable database '.
'is configured).');
}
return pht('Phabricator is in read-only mode.');
}
public static function getReadOnlyURI() {

View file

@ -57,16 +57,12 @@ abstract class PhabricatorLiskDAO extends LiskDAO {
$is_readonly = PhabricatorEnv::isReadOnly();
if ($is_readonly && ($mode != 'r')) {
throw new Exception(
pht(
'Attempting to establish write-mode connection from a read-only '.
'page (to database "%s").',
$database));
$this->raiseImproperWrite($database);
}
$refs = PhabricatorDatabaseRef::loadAll();
if ($refs) {
$connection = $this->newClusterConnection($database);
$connection = $this->newClusterConnection($database, $mode);
} else {
$connection = $this->newBasicConnection($database, $mode, $namespace);
}
@ -101,15 +97,31 @@ abstract class PhabricatorLiskDAO extends LiskDAO {
));
}
private function newClusterConnection($database) {
private function newClusterConnection($database, $mode) {
$master = PhabricatorDatabaseRef::getMasterDatabaseRef();
if (!$master) {
// TODO: Implicitly degrade to read-only mode.
throw new Exception(pht('No master in database cluster config!'));
if ($master) {
return $master->newApplicationConnection($database);
}
return $master->newApplicationConnection($database);
$replica = PhabricatorDatabaseRef::getReplicaDatabaseRef();
if (!$replica) {
throw new Exception(
pht('No valid databases are configured!'));
}
$connection = $replica->newApplicationConnection($database);
$connection->setReadOnly(true);
return $connection;
}
private function raiseImproperWrite($database) {
throw new PhabricatorClusterImproperWriteException(
pht(
'Unable to establish a write-mode connection (to application '.
'database "%s") because Phabricator is in read-only mode. Whatever '.
'you are trying to do does not function correctly in read-only mode.',
$database));
}