diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -1987,6 +1987,9 @@ '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 @@ 'PhabricatorChunkedFileStorageEngine' => 'PhabricatorFileStorageEngine', 'PhabricatorClusterConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorClusterDatabasesConfigOptionType' => 'PhabricatorConfigJSONOptionType', + 'PhabricatorClusterException' => 'Exception', + 'PhabricatorClusterExceptionHandler' => 'PhabricatorRequestExceptionHandler', + 'PhabricatorClusterImproperWriteException' => 'PhabricatorClusterException', 'PhabricatorColumnsEditField' => 'PhabricatorPHIDListEditField', 'PhabricatorCommentEditEngineExtension' => 'PhabricatorEditEngineExtension', 'PhabricatorCommentEditField' => 'PhabricatorEditField', diff --git a/src/applications/system/controller/PhabricatorSystemReadOnlyController.php b/src/applications/system/controller/PhabricatorSystemReadOnlyController.php --- a/src/applications/system/controller/PhabricatorSystemReadOnlyController.php +++ b/src/applications/system/controller/PhabricatorSystemReadOnlyController.php @@ -25,6 +25,27 @@ '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 @@ $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) diff --git a/src/infrastructure/cluster/PhabricatorClusterException.php b/src/infrastructure/cluster/PhabricatorClusterException.php new file mode 100644 --- /dev/null +++ b/src/infrastructure/cluster/PhabricatorClusterException.php @@ -0,0 +1,8 @@ +getViewer($request); + + $title = $ex->getExceptionTitle(); + + return id(new AphrontDialogView()) + ->setTitle($title) + ->setUser($viewer) + ->appendParagraph($ex->getMessage()) + ->addCancelButton('/', pht('Proceed With Caution')); + } + +} diff --git a/src/infrastructure/cluster/PhabricatorClusterImproperWriteException.php b/src/infrastructure/cluster/PhabricatorClusterImproperWriteException.php new file mode 100644 --- /dev/null +++ b/src/infrastructure/cluster/PhabricatorClusterImproperWriteException.php @@ -0,0 +1,10 @@ +getDisabled()) { + continue; + } + if ($ref->getIsMaster()) { + continue; + } + return $ref; + } + + return null; + } + private function newConnection(array $options) { $spec = $options + array( 'user' => $this->getUser(), diff --git a/src/infrastructure/env/PhabricatorEnv.php b/src/infrastructure/env/PhabricatorEnv.php --- a/src/infrastructure/env/PhabricatorEnv.php +++ b/src/infrastructure/env/PhabricatorEnv.php @@ -60,6 +60,7 @@ private static $readOnlyReason; const READONLY_CONFIG = 'config'; + const READONLY_MASTERLESS = 'masterless'; /** * @phutil-external-symbol class PhabricatorStartup @@ -213,6 +214,11 @@ $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 @@ } 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() { diff --git a/src/infrastructure/storage/lisk/PhabricatorLiskDAO.php b/src/infrastructure/storage/lisk/PhabricatorLiskDAO.php --- a/src/infrastructure/storage/lisk/PhabricatorLiskDAO.php +++ b/src/infrastructure/storage/lisk/PhabricatorLiskDAO.php @@ -57,16 +57,12 @@ $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 @@ )); } - private function newClusterConnection($database) { + private function newClusterConnection($database, $mode) { $master = PhabricatorDatabaseRef::getMasterDatabaseRef(); + if ($master) { + return $master->newApplicationConnection($database); + } - if (!$master) { - // TODO: Implicitly degrade to read-only mode. - throw new Exception(pht('No master in database cluster config!')); + $replica = PhabricatorDatabaseRef::getReplicaDatabaseRef(); + if (!$replica) { + throw new Exception( + pht('No valid databases are configured!')); } - return $master->newApplicationConnection($database); + $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)); }