diff --git a/src/applications/config/controller/PhabricatorConfigClusterDatabasesController.php b/src/applications/config/controller/PhabricatorConfigClusterDatabasesController.php --- a/src/applications/config/controller/PhabricatorConfigClusterDatabasesController.php +++ b/src/applications/config/controller/PhabricatorConfigClusterDatabasesController.php @@ -203,7 +203,7 @@ ->setIcon('fa-book') ->setHref($doc_href) ->setTag('a') - ->setText(pht('Database Clustering Documentation'))); + ->setText(pht('Documentation'))); return id(new PHUIObjectBoxView()) ->setHeader($header) diff --git a/src/applications/diffusion/management/DiffusionRepositoryClusterManagementPanel.php b/src/applications/diffusion/management/DiffusionRepositoryClusterManagementPanel.php --- a/src/applications/diffusion/management/DiffusionRepositoryClusterManagementPanel.php +++ b/src/applications/diffusion/management/DiffusionRepositoryClusterManagementPanel.php @@ -14,7 +14,116 @@ } public function buildManagementPanelContent() { - return pht('TODO: Cluster configuration management.'); + $repository = $this->getRepository(); + $viewer = $this->getViewer(); + + $service_phid = $repository->getAlmanacServicePHID(); + if ($service_phid) { + $service = id(new AlmanacServiceQuery()) + ->setViewer($viewer) + ->withServiceTypes( + array( + AlmanacClusterRepositoryServiceType::SERVICETYPE, + )) + ->withPHIDs(array($service_phid)) + ->needBindings(true) + ->executeOne(); + if (!$service) { + // TODO: Viewer may not have permission to see the service, or it may + // be invalid? Raise some more useful error here? + throw new Exception(pht('Unable to load cluster service.')); + } + } else { + $service = null; + } + + Javelin::initBehavior('phabricator-tooltips'); + + $rows = array(); + if ($service) { + $bindings = $service->getBindings(); + $bindings = mgroup($bindings, 'getDevicePHID'); + + foreach ($bindings as $binding_group) { + $all_disabled = true; + foreach ($binding_group as $binding) { + if (!$binding->getIsDisabled()) { + $all_disabled = false; + break; + } + } + + $any_binding = head($binding_group); + + if ($all_disabled) { + $binding_icon = 'fa-times grey'; + $binding_tip = pht('Disabled'); + } else { + $binding_icon = 'fa-folder-open green'; + $binding_tip = pht('Active'); + } + + $binding_icon = id(new PHUIIconView()) + ->setIcon($binding_icon) + ->addSigil('has-tooltip') + ->setMetadata( + array( + 'tip' => $binding_tip, + )); + + $device = $any_binding->getDevice(); + + $rows[] = array( + $binding_icon, + phutil_tag( + 'a', + array( + 'href' => $device->getURI(), + ), + $device->getName()), + ); + } + } + + $table = id(new AphrontTableView($rows)) + ->setNoDataString(pht('This is not a cluster repository.')) + ->setHeaders( + array( + null, + pht('Device'), + )) + ->setColumnClasses( + array( + null, + 'wide', + )); + + $doc_href = PhabricatorEnv::getDoclink('Cluster: Repositories'); + + $header = id(new PHUIHeaderView()) + ->setHeader(pht('Cluster Status')) + ->addActionLink( + id(new PHUIButtonView()) + ->setIcon('fa-book') + ->setHref($doc_href) + ->setTag('a') + ->setText(pht('Documentation'))); + + if ($service) { + $header->setSubheader( + pht( + 'This repository is hosted on %s.', + phutil_tag( + 'a', + array( + 'href' => $service->getURI(), + ), + $service->getName()))); + } + + return id(new PHUIObjectBoxView()) + ->setHeader($header) + ->setTable($table); } } diff --git a/src/applications/repository/daemon/PhabricatorRepositoryPullLocalDaemon.php b/src/applications/repository/daemon/PhabricatorRepositoryPullLocalDaemon.php --- a/src/applications/repository/daemon/PhabricatorRepositoryPullLocalDaemon.php +++ b/src/applications/repository/daemon/PhabricatorRepositoryPullLocalDaemon.php @@ -398,6 +398,10 @@ $services = id(new AlmanacServiceQuery()) ->setViewer($this->getViewer()) ->withPHIDs($service_phids) + ->withServiceTypes( + array( + AlmanacClusterRepositoryServiceType::SERVICETYPE, + )) ->needBindings(true) ->execute(); $services = mpull($services, null, 'getPHID'); @@ -422,9 +426,9 @@ } $bindings = $service->getBindings(); - $bindings = mpull($bindings, null, 'getDevicePHID'); - $binding = idx($bindings, $device_phid); - if (!$binding) { + $bindings = mgroup($bindings, 'getDevicePHID'); + $bindings = idx($bindings, $device_phid); + if (!$bindings) { $this->log( pht( 'Repository "%s" is on cluster service "%s", but that service '. @@ -437,7 +441,15 @@ continue; } - if ($binding->getIsDisabled()) { + $all_disabled = true; + foreach ($bindings as $binding) { + if (!$binding->getIsDisabled()) { + $all_disabled = false; + break; + } + } + + if ($all_disabled) { $this->log( pht( 'Repository "%s" is on cluster service "%s", but the binding '. diff --git a/src/docs/user/cluster/cluster.diviner b/src/docs/user/cluster/cluster.diviner --- a/src/docs/user/cluster/cluster.diviner +++ b/src/docs/user/cluster/cluster.diviner @@ -26,6 +26,7 @@ The remainder of this document summarizes how to add redundancy to each service and where your efforts are likely to have the greatest impact. + Cluster: Databases ================= @@ -38,3 +39,19 @@ the master, and to quickly promote the replica as a replacement. For details, see @{article:Cluster: Databases}. + + +Cluster: Repositories +===================== + +Configuring multiple repository hosts is complex. + +Repository replicas are important for availability if you host repositories +on Phabricator, but less important if you host repositories elsewhere +(instead, you should focus on making that service more available). + +The distributed nature of Git and Mercurial tend to mean that they are +naturally somewhat resistant to data loss: every clone of a repository includes +the entire history. + +For details, see @{article:Cluster: Repositories}. diff --git a/src/docs/user/cluster/cluster_repositories.diviner b/src/docs/user/cluster/cluster_repositories.diviner new file mode 100644 --- /dev/null +++ b/src/docs/user/cluster/cluster_repositories.diviner @@ -0,0 +1,86 @@ +@title Cluster: Repositories +@group intro + +Configuring Phabricator to use multiple repository hosts. + +Overview +======== + +WARNING: This feature is a very early prototype; the features this document +describes are mostly speculative fantasy. + +If you use Git or Mercurial, you can deploy Phabricator with multiple +repository hosts, configured so that each host is readable and writable. The +advantages of doing this are: + + - you can completely survive the loss of repository hosts; + - reads and writes can scale across multiple machines; and + - read and write performance across multiple geographic regions may improve. + +This configuration is complex, and many installs do not need to pursue it. + +This configuration is not currently supported with Subversion. + + +Repository Hosts +================ + +Repository hosts must run a complete, fully configured copy of Phabricator, +including a webserver. If you make repositories available over SSH, they must +also run a properly configured `sshd`. + +Generally, these hosts will run the same set of services and configuration that +web hosts run. If you prefer, you can overlay these services and put web and +repository services on the same hosts. + +When a user requests information about a repository that can only be satisfied +by examining a repository working copy, the webserver receiving the reqeust +will make an HTTP service call to a repository server which hosts the +repository to retrieve the data it needs. It will use the result of this query +to respond to the user. + + +How Reads and Writes Work +========================= + +Phabricator repository replicas are multi-master: every node is readable and +writable, and a cluster of nodes can (almost always) survive the loss of any +arbitrary subset of nodes so long as at least one node is still alive. + +Phabricator maintains an internal version for each repository, and increments +it when the repository is mutated. + +Before responding to a read, replicas make sure their version of the repository +is up to date (no node in the cluster has a newer version of the repository). +If it isn't, they block the read until they can complete a fetch. + +Before responding to a write, replicas obtain a global lock, perform the same +version check and fetch if necessary, then allow the write to continue. + + +Backups +====== + +Even if you configure clustering, you should still consider retaining separate +backup snapshots. Replicas protect you from data loss if you lose a host, but +they do not let you rewind time to recover from data mutation mistakes. + +If something issues a `--force` push that destroys branch heads, the mutation +will propagate to the replicas. + +You may be able to manually restore the branches by using tools like the +Phabricator push log or the Git reflog so it is less important to retain +repository snapshots than database snapshots, but it is still possible for +data to be lost permanently, especially if you don't notice the problem for +some time. + +Retaining separate backup snapshots will improve your ability to recover more +data more easily in a wider range of disaster situations. + + +Next Steps +========== + +Continue by: + + - returning to @{article:Clustering Introduction}.