mirror of
https://we.phorge.it/source/phorge.git
synced 2024-11-30 02:32:42 +01:00
Allow repository cluster bindings to be marked as not "writable", making them read-only
Summary: Depends on D19356. Fixes T10883. Ref T13120. - Add a "writable" property to the bindings, defaulting to "true" with a nice dropdown. - When selecting hosts, allow callers to request a writable host. - If the caller wants a writable host, only return hosts if they're writable. - In SVN and Mercurial, we sometimes return only writable hosts when we //could// return read-only hosts, but figuring out if these request are read-only or read-write is currently tricky. Since these repositories can't really cluster yet, this shouldn't matter too much today. Test Plan: - Without any config changes, viewed repositories via web UI and pushed/pulled via SSH and HTTP. - Made all nodes in the cluster read-only by disabling "writable", pulled and hit the web UI (worked), tried to push via SSH and HTTP (got errors about read-only). - Put everything back, pulled and pushed. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13120, T10883 Differential Revision: https://secure.phabricator.com/D19357
This commit is contained in:
parent
7c7e6d555b
commit
6556536d06
9 changed files with 114 additions and 25 deletions
|
@ -57,6 +57,11 @@ final class AlmanacClusterRepositoryServiceType
|
||||||
'protocol' => id(new PhabricatorSelectEditField())
|
'protocol' => id(new PhabricatorSelectEditField())
|
||||||
->setOptions(ipull($protocols, 'value', 'value'))
|
->setOptions(ipull($protocols, 'value', 'value'))
|
||||||
->setValue($default_value),
|
->setValue($default_value),
|
||||||
|
'writable' => id(new PhabricatorBoolEditField())
|
||||||
|
->setOptions(
|
||||||
|
pht('Prevent Writes'),
|
||||||
|
pht('Allow Writes'))
|
||||||
|
->setValue(true),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -437,6 +437,7 @@ final class DiffusionServeController extends DiffusionController {
|
||||||
'http',
|
'http',
|
||||||
'https',
|
'https',
|
||||||
),
|
),
|
||||||
|
'writable' => !$this->isReadOnlyRequest($repository),
|
||||||
));
|
));
|
||||||
if ($uri) {
|
if ($uri) {
|
||||||
$future = $this->getRequest()->newClusterProxyFuture($uri);
|
$future = $this->getRequest()->newClusterProxyFuture($uri);
|
||||||
|
|
|
@ -29,7 +29,7 @@ final class DiffusionGitReceivePackSSHWorkflow extends DiffusionGitSSHWorkflow {
|
||||||
->setLog($this);
|
->setLog($this);
|
||||||
|
|
||||||
if ($this->shouldProxy()) {
|
if ($this->shouldProxy()) {
|
||||||
$command = $this->getProxyCommand();
|
$command = $this->getProxyCommand(true);
|
||||||
$did_synchronize = false;
|
$did_synchronize = false;
|
||||||
|
|
||||||
if ($device) {
|
if ($device) {
|
||||||
|
|
|
@ -22,7 +22,7 @@ final class DiffusionGitUploadPackSSHWorkflow extends DiffusionGitSSHWorkflow {
|
||||||
$is_proxy = $this->shouldProxy();
|
$is_proxy = $this->shouldProxy();
|
||||||
|
|
||||||
if ($is_proxy) {
|
if ($is_proxy) {
|
||||||
$command = $this->getProxyCommand();
|
$command = $this->getProxyCommand(false);
|
||||||
|
|
||||||
if ($device) {
|
if ($device) {
|
||||||
$this->writeClusterEngineLogMessage(
|
$this->writeClusterEngineLogMessage(
|
||||||
|
|
|
@ -45,7 +45,10 @@ final class DiffusionMercurialServeSSHWorkflow
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($this->shouldProxy()) {
|
if ($this->shouldProxy()) {
|
||||||
$command = $this->getProxyCommand();
|
// NOTE: For now, we're always requesting a writable node. The request
|
||||||
|
// may not actually need one, but we can't currently determine whether
|
||||||
|
// it is read-only or not at this phase of evaluation.
|
||||||
|
$command = $this->getProxyCommand(true);
|
||||||
} else {
|
} else {
|
||||||
$command = csprintf(
|
$command = csprintf(
|
||||||
'hg -R %s serve --stdio',
|
'hg -R %s serve --stdio',
|
||||||
|
|
|
@ -5,7 +5,7 @@ abstract class DiffusionSSHWorkflow extends PhabricatorSSHWorkflow {
|
||||||
private $args;
|
private $args;
|
||||||
private $repository;
|
private $repository;
|
||||||
private $hasWriteAccess;
|
private $hasWriteAccess;
|
||||||
private $proxyURI;
|
private $shouldProxy;
|
||||||
private $baseRequestPath;
|
private $baseRequestPath;
|
||||||
|
|
||||||
public function getRepository() {
|
public function getRepository() {
|
||||||
|
@ -69,18 +69,34 @@ abstract class DiffusionSSHWorkflow extends PhabricatorSSHWorkflow {
|
||||||
return php_uname('n');
|
return php_uname('n');
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function getTargetDeviceName() {
|
|
||||||
// TODO: This should use the correct device identity.
|
|
||||||
$uri = new PhutilURI($this->proxyURI);
|
|
||||||
return $uri->getDomain();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function shouldProxy() {
|
protected function shouldProxy() {
|
||||||
return (bool)$this->proxyURI;
|
return $this->shouldProxy;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function getProxyCommand() {
|
protected function getProxyCommand($for_write) {
|
||||||
$uri = new PhutilURI($this->proxyURI);
|
$viewer = $this->getSSHUser();
|
||||||
|
$repository = $this->getRepository();
|
||||||
|
|
||||||
|
$is_cluster_request = $this->getIsClusterRequest();
|
||||||
|
|
||||||
|
$uri = $repository->getAlmanacServiceURI(
|
||||||
|
$viewer,
|
||||||
|
array(
|
||||||
|
'neverProxy' => $is_cluster_request,
|
||||||
|
'protocols' => array(
|
||||||
|
'ssh',
|
||||||
|
),
|
||||||
|
'writable' => $for_write,
|
||||||
|
));
|
||||||
|
|
||||||
|
if (!$uri) {
|
||||||
|
throw new Exception(
|
||||||
|
pht(
|
||||||
|
'Failed to generate an intracluster proxy URI even though this '.
|
||||||
|
'request was routed as a proxy request.'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$uri = new PhutilURI($uri);
|
||||||
|
|
||||||
$username = AlmanacKeys::getClusterSSHUser();
|
$username = AlmanacKeys::getClusterSSHUser();
|
||||||
if ($username === null) {
|
if ($username === null) {
|
||||||
|
@ -148,6 +164,15 @@ abstract class DiffusionSSHWorkflow extends PhabricatorSSHWorkflow {
|
||||||
$repository = $this->identifyRepository();
|
$repository = $this->identifyRepository();
|
||||||
$this->setRepository($repository);
|
$this->setRepository($repository);
|
||||||
|
|
||||||
|
// NOTE: Here, we're just figuring out if this is a proxyable request to
|
||||||
|
// a clusterized repository or not. We don't (and can't) use the URI we get
|
||||||
|
// back directly.
|
||||||
|
|
||||||
|
// For example, we may get a read-only URI here but be handling a write
|
||||||
|
// request. We only care if we get back `null` (which means we should
|
||||||
|
// handle the request locally) or anything else (which means we should
|
||||||
|
// proxy it to an appropriate device).
|
||||||
|
|
||||||
$is_cluster_request = $this->getIsClusterRequest();
|
$is_cluster_request = $this->getIsClusterRequest();
|
||||||
$uri = $repository->getAlmanacServiceURI(
|
$uri = $repository->getAlmanacServiceURI(
|
||||||
$viewer,
|
$viewer,
|
||||||
|
@ -157,10 +182,7 @@ abstract class DiffusionSSHWorkflow extends PhabricatorSSHWorkflow {
|
||||||
'ssh',
|
'ssh',
|
||||||
),
|
),
|
||||||
));
|
));
|
||||||
|
$this->shouldProxy = (bool)$uri;
|
||||||
if ($uri) {
|
|
||||||
$this->proxyURI = $uri;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return $this->executeRepositoryOperations();
|
return $this->executeRepositoryOperations();
|
||||||
|
|
|
@ -148,7 +148,10 @@ final class DiffusionSubversionServeSSHWorkflow
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($this->shouldProxy()) {
|
if ($this->shouldProxy()) {
|
||||||
$command = $this->getProxyCommand();
|
// NOTE: We're always requesting a writable device here. The request
|
||||||
|
// might be read-only, but we can't currently tell, and SVN requests
|
||||||
|
// can mix reads and writes.
|
||||||
|
$command = $this->getProxyCommand(true);
|
||||||
$this->isProxying = true;
|
$this->isProxying = true;
|
||||||
$cwd = null;
|
$cwd = null;
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -1909,10 +1909,12 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO
|
||||||
array(
|
array(
|
||||||
'neverProxy' => 'bool',
|
'neverProxy' => 'bool',
|
||||||
'protocols' => 'list<string>',
|
'protocols' => 'list<string>',
|
||||||
|
'writable' => 'optional bool',
|
||||||
));
|
));
|
||||||
|
|
||||||
$never_proxy = $options['neverProxy'];
|
$never_proxy = $options['neverProxy'];
|
||||||
$protocols = $options['protocols'];
|
$protocols = $options['protocols'];
|
||||||
|
$writable = idx($options, 'writable', false);
|
||||||
|
|
||||||
$cache_key = $this->getAlmanacServiceCacheKey();
|
$cache_key = $this->getAlmanacServiceCacheKey();
|
||||||
if (!$cache_key) {
|
if (!$cache_key) {
|
||||||
|
@ -1958,7 +1960,7 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isset($protocol_map[$uri['protocol']])) {
|
if (isset($protocol_map[$uri['protocol']])) {
|
||||||
$results[] = new PhutilURI($uri['uri']);
|
$results[] = $uri;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1989,8 +1991,29 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If we require a writable device, remove URIs which aren't writable.
|
||||||
|
if ($writable) {
|
||||||
|
foreach ($results as $key => $uri) {
|
||||||
|
if (!$uri['writable']) {
|
||||||
|
unset($results[$key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$results) {
|
||||||
|
throw new Exception(
|
||||||
|
pht(
|
||||||
|
'This repository ("%s") is not writable with the given '.
|
||||||
|
'protocols (%s). The Almanac service for this repository has no '.
|
||||||
|
'writable bindings that support these protocols.',
|
||||||
|
$this->getDisplayName(),
|
||||||
|
implode(', ', $protocols)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
shuffle($results);
|
shuffle($results);
|
||||||
return head($results);
|
|
||||||
|
$result = head($results);
|
||||||
|
return $result['uri'];
|
||||||
}
|
}
|
||||||
|
|
||||||
public function supportsSynchronization() {
|
public function supportsSynchronization() {
|
||||||
|
@ -2009,7 +2032,14 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO
|
||||||
}
|
}
|
||||||
|
|
||||||
$repository_phid = $this->getPHID();
|
$repository_phid = $this->getPHID();
|
||||||
return "diffusion.repository({$repository_phid}).service({$service_phid})";
|
|
||||||
|
$parts = array(
|
||||||
|
"repo({$repository_phid})",
|
||||||
|
"serv({$service_phid})",
|
||||||
|
'v2',
|
||||||
|
);
|
||||||
|
|
||||||
|
return implode('.', $parts);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function buildAlmanacServiceURIs() {
|
private function buildAlmanacServiceURIs() {
|
||||||
|
@ -2038,6 +2068,7 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO
|
||||||
'protocol' => $protocol,
|
'protocol' => $protocol,
|
||||||
'uri' => (string)$uri,
|
'uri' => (string)$uri,
|
||||||
'device' => $device_name,
|
'device' => $device_name,
|
||||||
|
'writable' => (bool)$binding->getAlmanacPropertyValue('writable'),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2091,6 +2122,10 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO
|
||||||
'http',
|
'http',
|
||||||
'https',
|
'https',
|
||||||
),
|
),
|
||||||
|
|
||||||
|
// At least today, no Conduit call can ever write to a repository,
|
||||||
|
// so it's fine to send anything to a read-only node.
|
||||||
|
'writable' => false,
|
||||||
));
|
));
|
||||||
if ($uri === null) {
|
if ($uri === null) {
|
||||||
return null;
|
return null;
|
||||||
|
|
|
@ -221,13 +221,33 @@ synchronized.
|
||||||
Contracting a Cluster
|
Contracting a Cluster
|
||||||
=====================
|
=====================
|
||||||
|
|
||||||
To reduce the size of an existing cluster, follow these general steps:
|
If you want to remove working devices from a cluster (for example, to take
|
||||||
|
hosts down for maintenance), first do this for each device:
|
||||||
|
|
||||||
- Disable the bindings from the service to the dead device in Almanac.
|
- Change the `writable` property on the bindings to "Prevent Writes".
|
||||||
|
- Wait a few moments until the cluster synchronizes (see
|
||||||
|
"Monitoring Services" below).
|
||||||
|
|
||||||
|
This will ensure that the device you're about to remove is not the only cluster
|
||||||
|
leader, even if the cluster is receiving a high write volume. You can skip this
|
||||||
|
step if the device isn't working property to start with.
|
||||||
|
|
||||||
|
Once you've stopped writes and waited for synchronization (or if the hosts are
|
||||||
|
not working in the first place) do this for each device:
|
||||||
|
|
||||||
|
- Disable the bindings from the service to the device in Almanac.
|
||||||
|
|
||||||
If you are removing a device because it failed abruptly (or removing several
|
If you are removing a device because it failed abruptly (or removing several
|
||||||
devices at once) it is possible that some repositories will have lost all their
|
devices at once; or you skip the "Prevent Writes" step), it is possible that
|
||||||
leaders. See "Loss of Leaders" below to understand and resolve this.
|
some repositories will have lost all their leaders. See "Loss of Leaders" below
|
||||||
|
to understand and resolve this.
|
||||||
|
|
||||||
|
If you want to put the hosts back in service later:
|
||||||
|
|
||||||
|
- Enable the bindings again.
|
||||||
|
- Change `writable` back to "Allow Writes".
|
||||||
|
|
||||||
|
This will restore the cluster to the original state.
|
||||||
|
|
||||||
|
|
||||||
Monitoring Services
|
Monitoring Services
|
||||||
|
|
Loading…
Reference in a new issue