@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

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

+145 -14
+6
src/__phutil_library_map__.php
··· 1987 1987 'PhabricatorChunkedFileStorageEngine' => 'applications/files/engine/PhabricatorChunkedFileStorageEngine.php', 1988 1988 'PhabricatorClusterConfigOptions' => 'applications/config/option/PhabricatorClusterConfigOptions.php', 1989 1989 'PhabricatorClusterDatabasesConfigOptionType' => 'infrastructure/cluster/PhabricatorClusterDatabasesConfigOptionType.php', 1990 + 'PhabricatorClusterException' => 'infrastructure/cluster/PhabricatorClusterException.php', 1991 + 'PhabricatorClusterExceptionHandler' => 'infrastructure/cluster/PhabricatorClusterExceptionHandler.php', 1992 + 'PhabricatorClusterImproperWriteException' => 'infrastructure/cluster/PhabricatorClusterImproperWriteException.php', 1990 1993 'PhabricatorColumnProxyInterface' => 'applications/project/interface/PhabricatorColumnProxyInterface.php', 1991 1994 'PhabricatorColumnsEditField' => 'applications/transactions/editfield/PhabricatorColumnsEditField.php', 1992 1995 'PhabricatorCommentEditEngineExtension' => 'applications/transactions/engineextension/PhabricatorCommentEditEngineExtension.php', ··· 6397 6400 'PhabricatorChunkedFileStorageEngine' => 'PhabricatorFileStorageEngine', 6398 6401 'PhabricatorClusterConfigOptions' => 'PhabricatorApplicationConfigOptions', 6399 6402 'PhabricatorClusterDatabasesConfigOptionType' => 'PhabricatorConfigJSONOptionType', 6403 + 'PhabricatorClusterException' => 'Exception', 6404 + 'PhabricatorClusterExceptionHandler' => 'PhabricatorRequestExceptionHandler', 6405 + 'PhabricatorClusterImproperWriteException' => 'PhabricatorClusterException', 6400 6406 'PhabricatorColumnsEditField' => 'PhabricatorPHIDListEditField', 6401 6407 'PhabricatorCommentEditEngineExtension' => 'PhabricatorEditEngineExtension', 6402 6408 'PhabricatorCommentEditField' => 'PhabricatorEditField',
+23 -2
src/applications/system/controller/PhabricatorSystemReadOnlyController.php
··· 25 25 'has been turned on by rolling your chair away from your desk and '. 26 26 'yelling "Hey! Why is Phabricator in read-only mode??!" using '. 27 27 'your very loudest outside voice.'); 28 + $body[] = pht( 29 + 'This mode is active because it is enabled in the configuration '. 30 + 'option "%s".', 31 + phutil_tag('tt', array(), 'cluster.read-only')); 32 + $button = pht('Wait Patiently'); 33 + break; 34 + case PhabricatorEnv::READONLY_MASTERLESS: 35 + $title = pht('No Writable Database'); 36 + $body[] = pht( 37 + 'Phabricator is currently configured with no writable ("master") '. 38 + 'database, so it can not write new information anywhere. '. 39 + 'Phabricator will run in read-only mode until an administrator '. 40 + 'reconfigures it with a writable database.'); 41 + $body[] = pht( 42 + 'This usually occurs when an administrator is actively working on '. 43 + 'fixing a temporary configuration or deployment problem.'); 44 + $body[] = pht( 45 + 'This mode is active because no database has a "%s" role in '. 46 + 'the configuration option "%s".', 47 + phutil_tag('tt', array(), 'master'), 48 + phutil_tag('tt', array(), 'cluster.databases')); 28 49 $button = pht('Wait Patiently'); 29 50 break; 30 51 default: ··· 33 54 34 55 $body[] = pht( 35 56 'In read-only mode you can read existing information, but you will not '. 36 - 'be able to edit information or create new information until this mode '. 37 - 'is disabled.'); 57 + 'be able to edit objects or create new objects until this mode is '. 58 + 'disabled.'); 38 59 39 60 $dialog = $this->newDialog() 40 61 ->setTitle($title)
+8
src/infrastructure/cluster/PhabricatorClusterException.php
··· 1 + <?php 2 + 3 + abstract class PhabricatorClusterException 4 + extends Exception { 5 + 6 + abstract public function getExceptionTitle(); 7 + 8 + }
+35
src/infrastructure/cluster/PhabricatorClusterExceptionHandler.php
··· 1 + <?php 2 + 3 + final class PhabricatorClusterExceptionHandler 4 + extends PhabricatorRequestExceptionHandler { 5 + 6 + public function getRequestExceptionHandlerPriority() { 7 + return 300000; 8 + } 9 + 10 + public function getRequestExceptionHandlerDescription() { 11 + return pht('Handles runtime problems with cluster configuration.'); 12 + } 13 + 14 + public function canHandleRequestException( 15 + AphrontRequest $request, 16 + Exception $ex) { 17 + return ($ex instanceof PhabricatorClusterException); 18 + } 19 + 20 + public function handleRequestException( 21 + AphrontRequest $request, 22 + Exception $ex) { 23 + 24 + $viewer = $this->getViewer($request); 25 + 26 + $title = $ex->getExceptionTitle(); 27 + 28 + return id(new AphrontDialogView()) 29 + ->setTitle($title) 30 + ->setUser($viewer) 31 + ->appendParagraph($ex->getMessage()) 32 + ->addCancelButton('/', pht('Proceed With Caution')); 33 + } 34 + 35 + }
+10
src/infrastructure/cluster/PhabricatorClusterImproperWriteException.php
··· 1 + <?php 2 + 3 + final class PhabricatorClusterImproperWriteException 4 + extends PhabricatorClusterException { 5 + 6 + public function getExceptionTitle() { 7 + return pht('Improper Cluster Write'); 8 + } 9 + 10 + }
+25
src/infrastructure/cluster/PhabricatorDatabaseRef.php
··· 347 347 return null; 348 348 } 349 349 350 + public static function getReplicaDatabaseRef() { 351 + $refs = self::loadAll(); 352 + 353 + if (!$refs) { 354 + return null; 355 + } 356 + 357 + // TODO: We may have multiple replicas to choose from, and could make 358 + // more of an effort to pick the "best" one here instead of always 359 + // picking the first one. Once we've picked one, we should try to use 360 + // the same replica for the rest of the request, though. 361 + 362 + foreach ($refs as $ref) { 363 + if ($ref->getDisabled()) { 364 + continue; 365 + } 366 + if ($ref->getIsMaster()) { 367 + continue; 368 + } 369 + return $ref; 370 + } 371 + 372 + return null; 373 + } 374 + 350 375 private function newConnection(array $options) { 351 376 $spec = $options + array( 352 377 'user' => $this->getUser(),
+15 -1
src/infrastructure/env/PhabricatorEnv.php
··· 60 60 private static $readOnlyReason; 61 61 62 62 const READONLY_CONFIG = 'config'; 63 + const READONLY_MASTERLESS = 'masterless'; 63 64 64 65 /** 65 66 * @phutil-external-symbol class PhabricatorStartup ··· 211 212 212 213 foreach ($site_sources as $site_source) { 213 214 $stack->pushSource($site_source); 215 + } 216 + 217 + $master = PhabricatorDatabaseRef::getMasterDatabaseRef(); 218 + if (!$master) { 219 + self::setReadOnly(true, self::READONLY_MASTERLESS); 214 220 } 215 221 216 222 try { ··· 456 462 } 457 463 458 464 public static function getReadOnlyMessage() { 459 - return pht('Phabricator is currently in read-only mode.'); 465 + $reason = self::getReadOnlyReason(); 466 + switch ($reason) { 467 + case self::READONLY_MASTERLESS: 468 + return pht( 469 + 'Phabricator is in read-only mode (no writable database '. 470 + 'is configured).'); 471 + } 472 + 473 + return pht('Phabricator is in read-only mode.'); 460 474 } 461 475 462 476 public static function getReadOnlyURI() {
+23 -11
src/infrastructure/storage/lisk/PhabricatorLiskDAO.php
··· 57 57 $is_readonly = PhabricatorEnv::isReadOnly(); 58 58 59 59 if ($is_readonly && ($mode != 'r')) { 60 - throw new Exception( 61 - pht( 62 - 'Attempting to establish write-mode connection from a read-only '. 63 - 'page (to database "%s").', 64 - $database)); 60 + $this->raiseImproperWrite($database); 65 61 } 66 62 67 63 $refs = PhabricatorDatabaseRef::loadAll(); 68 64 if ($refs) { 69 - $connection = $this->newClusterConnection($database); 65 + $connection = $this->newClusterConnection($database, $mode); 70 66 } else { 71 67 $connection = $this->newBasicConnection($database, $mode, $namespace); 72 68 } ··· 101 97 )); 102 98 } 103 99 104 - private function newClusterConnection($database) { 100 + private function newClusterConnection($database, $mode) { 105 101 $master = PhabricatorDatabaseRef::getMasterDatabaseRef(); 102 + if ($master) { 103 + return $master->newApplicationConnection($database); 104 + } 106 105 107 - if (!$master) { 108 - // TODO: Implicitly degrade to read-only mode. 109 - throw new Exception(pht('No master in database cluster config!')); 106 + $replica = PhabricatorDatabaseRef::getReplicaDatabaseRef(); 107 + if (!$replica) { 108 + throw new Exception( 109 + pht('No valid databases are configured!')); 110 110 } 111 111 112 - return $master->newApplicationConnection($database); 112 + $connection = $replica->newApplicationConnection($database); 113 + $connection->setReadOnly(true); 114 + 115 + return $connection; 116 + } 117 + 118 + private function raiseImproperWrite($database) { 119 + throw new PhabricatorClusterImproperWriteException( 120 + pht( 121 + 'Unable to establish a write-mode connection (to application '. 122 + 'database "%s") because Phabricator is in read-only mode. Whatever '. 123 + 'you are trying to do does not function correctly in read-only mode.', 124 + $database)); 113 125 } 114 126 115 127