Page MenuHomePhabricator

D17384.id.diff
No OneTemporary

D17384.id.diff

diff --git a/resources/sql/autopatches/20161130.search.02.rebuild.php b/resources/sql/autopatches/20161130.search.02.rebuild.php
--- a/resources/sql/autopatches/20161130.search.02.rebuild.php
+++ b/resources/sql/autopatches/20161130.search.02.rebuild.php
@@ -1,7 +1,15 @@
<?php
-$search_engine = PhabricatorFulltextStorageEngine::loadEngine();
-$use_mysql = ($search_engine instanceof PhabricatorMySQLFulltextStorageEngine);
+
+$use_mysql = false;
+
+$services = PhabricatorSearchService::getAllServices();
+foreach ($services as $service) {
+ $engine = $service->getEngine();
+ if ($engine instanceof PhabricatorMySQLFulltextStorageEngine) {
+ $use_mysql = true;
+ }
+}
if ($use_mysql) {
$field = new PhabricatorSearchDocumentField();
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
@@ -2259,12 +2259,15 @@
'PhabricatorChatLogQuery' => 'applications/chatlog/query/PhabricatorChatLogQuery.php',
'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',
- 'PhabricatorClusterImpossibleWriteException' => 'infrastructure/cluster/PhabricatorClusterImpossibleWriteException.php',
- 'PhabricatorClusterImproperWriteException' => 'infrastructure/cluster/PhabricatorClusterImproperWriteException.php',
- 'PhabricatorClusterStrandedException' => 'infrastructure/cluster/PhabricatorClusterStrandedException.php',
+ 'PhabricatorClusterDatabasesConfigOptionType' => 'infrastructure/cluster/config/PhabricatorClusterDatabasesConfigOptionType.php',
+ 'PhabricatorClusterException' => 'infrastructure/cluster/exception/PhabricatorClusterException.php',
+ 'PhabricatorClusterExceptionHandler' => 'infrastructure/cluster/exception/PhabricatorClusterExceptionHandler.php',
+ 'PhabricatorClusterImpossibleWriteException' => 'infrastructure/cluster/exception/PhabricatorClusterImpossibleWriteException.php',
+ 'PhabricatorClusterImproperWriteException' => 'infrastructure/cluster/exception/PhabricatorClusterImproperWriteException.php',
+ 'PhabricatorClusterNoHostForRoleException' => 'infrastructure/cluster/exception/PhabricatorClusterNoHostForRoleException.php',
+ 'PhabricatorClusterSearchConfigOptionType' => 'infrastructure/cluster/config/PhabricatorClusterSearchConfigOptionType.php',
+ 'PhabricatorClusterServiceHealthRecord' => 'infrastructure/cluster/PhabricatorClusterServiceHealthRecord.php',
+ 'PhabricatorClusterStrandedException' => 'infrastructure/cluster/exception/PhabricatorClusterStrandedException.php',
'PhabricatorColumnProxyInterface' => 'applications/project/interface/PhabricatorColumnProxyInterface.php',
'PhabricatorColumnsEditField' => 'applications/transactions/editfield/PhabricatorColumnsEditField.php',
'PhabricatorCommentEditEngineExtension' => 'applications/transactions/engineextension/PhabricatorCommentEditEngineExtension.php',
@@ -2310,6 +2313,7 @@
'PhabricatorConfigClusterDatabasesController' => 'applications/config/controller/PhabricatorConfigClusterDatabasesController.php',
'PhabricatorConfigClusterNotificationsController' => 'applications/config/controller/PhabricatorConfigClusterNotificationsController.php',
'PhabricatorConfigClusterRepositoriesController' => 'applications/config/controller/PhabricatorConfigClusterRepositoriesController.php',
+ 'PhabricatorConfigClusterSearchController' => 'applications/config/controller/PhabricatorConfigClusterSearchController.php',
'PhabricatorConfigCollectorsModule' => 'applications/config/module/PhabricatorConfigCollectorsModule.php',
'PhabricatorConfigColumnSchema' => 'applications/config/schema/PhabricatorConfigColumnSchema.php',
'PhabricatorConfigConfigPHIDType' => 'applications/config/phid/PhabricatorConfigConfigPHIDType.php',
@@ -2543,7 +2547,6 @@
'PhabricatorDashboardViewController' => 'applications/dashboard/controller/PhabricatorDashboardViewController.php',
'PhabricatorDataCacheSpec' => 'applications/cache/spec/PhabricatorDataCacheSpec.php',
'PhabricatorDataNotAttachedException' => 'infrastructure/storage/lisk/PhabricatorDataNotAttachedException.php',
- 'PhabricatorDatabaseHealthRecord' => 'infrastructure/cluster/PhabricatorDatabaseHealthRecord.php',
'PhabricatorDatabaseRef' => 'infrastructure/cluster/PhabricatorDatabaseRef.php',
'PhabricatorDatabaseRefParser' => 'infrastructure/cluster/PhabricatorDatabaseRefParser.php',
'PhabricatorDatabaseSetupCheck' => 'applications/config/check/PhabricatorDatabaseSetupCheck.php',
@@ -2651,6 +2654,8 @@
'PhabricatorEditorMultipleSetting' => 'applications/settings/setting/PhabricatorEditorMultipleSetting.php',
'PhabricatorEditorSetting' => 'applications/settings/setting/PhabricatorEditorSetting.php',
'PhabricatorElasticFulltextStorageEngine' => 'applications/search/fulltextstorage/PhabricatorElasticFulltextStorageEngine.php',
+ 'PhabricatorElasticSearchHost' => 'infrastructure/cluster/search/PhabricatorElasticSearchHost.php',
+ 'PhabricatorElasticSearchQueryBuilder' => 'applications/search/fulltextstorage/PhabricatorElasticSearchQueryBuilder.php',
'PhabricatorElasticSearchSetupCheck' => 'applications/config/check/PhabricatorElasticSearchSetupCheck.php',
'PhabricatorEmailAddressesSettingsPanel' => 'applications/settings/panel/PhabricatorEmailAddressesSettingsPanel.php',
'PhabricatorEmailContentSource' => 'applications/metamta/contentsource/PhabricatorEmailContentSource.php',
@@ -3073,6 +3078,7 @@
'PhabricatorMySQLConfigOptions' => 'applications/config/option/PhabricatorMySQLConfigOptions.php',
'PhabricatorMySQLFileStorageEngine' => 'applications/files/engine/PhabricatorMySQLFileStorageEngine.php',
'PhabricatorMySQLFulltextStorageEngine' => 'applications/search/fulltextstorage/PhabricatorMySQLFulltextStorageEngine.php',
+ 'PhabricatorMySQLSearchHost' => 'infrastructure/cluster/search/PhabricatorMySQLSearchHost.php',
'PhabricatorMySQLSetupCheck' => 'applications/config/check/PhabricatorMySQLSetupCheck.php',
'PhabricatorNamedQuery' => 'applications/search/storage/PhabricatorNamedQuery.php',
'PhabricatorNamedQueryQuery' => 'applications/search/query/PhabricatorNamedQueryQuery.php',
@@ -3762,7 +3768,6 @@
'PhabricatorSearchApplicationStorageEnginePanel' => 'applications/search/applicationpanel/PhabricatorSearchApplicationStorageEnginePanel.php',
'PhabricatorSearchBaseController' => 'applications/search/controller/PhabricatorSearchBaseController.php',
'PhabricatorSearchCheckboxesField' => 'applications/search/field/PhabricatorSearchCheckboxesField.php',
- 'PhabricatorSearchConfigOptions' => 'applications/search/config/PhabricatorSearchConfigOptions.php',
'PhabricatorSearchConstraintException' => 'applications/search/exception/PhabricatorSearchConstraintException.php',
'PhabricatorSearchController' => 'applications/search/controller/PhabricatorSearchController.php',
'PhabricatorSearchCustomFieldProxyField' => 'applications/search/field/PhabricatorSearchCustomFieldProxyField.php',
@@ -3785,6 +3790,7 @@
'PhabricatorSearchEngineExtensionModule' => 'applications/search/engineextension/PhabricatorSearchEngineExtensionModule.php',
'PhabricatorSearchEngineTestCase' => 'applications/search/engine/__tests__/PhabricatorSearchEngineTestCase.php',
'PhabricatorSearchField' => 'applications/search/field/PhabricatorSearchField.php',
+ 'PhabricatorSearchHost' => 'infrastructure/cluster/search/PhabricatorSearchHost.php',
'PhabricatorSearchHovercardController' => 'applications/search/controller/PhabricatorSearchHovercardController.php',
'PhabricatorSearchIndexVersion' => 'applications/search/storage/PhabricatorSearchIndexVersion.php',
'PhabricatorSearchIndexVersionDestructionEngineExtension' => 'applications/search/engineextension/PhabricatorSearchIndexVersionDestructionEngineExtension.php',
@@ -3804,6 +3810,7 @@
'PhabricatorSearchSchemaSpec' => 'applications/search/storage/PhabricatorSearchSchemaSpec.php',
'PhabricatorSearchScopeSetting' => 'applications/settings/setting/PhabricatorSearchScopeSetting.php',
'PhabricatorSearchSelectField' => 'applications/search/field/PhabricatorSearchSelectField.php',
+ 'PhabricatorSearchService' => 'infrastructure/cluster/search/PhabricatorSearchService.php',
'PhabricatorSearchStringListField' => 'applications/search/field/PhabricatorSearchStringListField.php',
'PhabricatorSearchSubscribersField' => 'applications/search/field/PhabricatorSearchSubscribersField.php',
'PhabricatorSearchTextField' => 'applications/search/field/PhabricatorSearchTextField.php',
@@ -7303,6 +7310,9 @@
'PhabricatorClusterExceptionHandler' => 'PhabricatorRequestExceptionHandler',
'PhabricatorClusterImpossibleWriteException' => 'PhabricatorClusterException',
'PhabricatorClusterImproperWriteException' => 'PhabricatorClusterException',
+ 'PhabricatorClusterNoHostForRoleException' => 'Exception',
+ 'PhabricatorClusterSearchConfigOptionType' => 'PhabricatorConfigJSONOptionType',
+ 'PhabricatorClusterServiceHealthRecord' => 'Phobject',
'PhabricatorClusterStrandedException' => 'PhabricatorClusterException',
'PhabricatorColumnsEditField' => 'PhabricatorPHIDListEditField',
'PhabricatorCommentEditEngineExtension' => 'PhabricatorEditEngineExtension',
@@ -7354,6 +7364,7 @@
'PhabricatorConfigClusterDatabasesController' => 'PhabricatorConfigController',
'PhabricatorConfigClusterNotificationsController' => 'PhabricatorConfigController',
'PhabricatorConfigClusterRepositoriesController' => 'PhabricatorConfigController',
+ 'PhabricatorConfigClusterSearchController' => 'PhabricatorConfigController',
'PhabricatorConfigCollectorsModule' => 'PhabricatorConfigModule',
'PhabricatorConfigColumnSchema' => 'PhabricatorConfigStorageSchema',
'PhabricatorConfigConfigPHIDType' => 'PhabricatorPHIDType',
@@ -7624,7 +7635,6 @@
'PhabricatorDashboardViewController' => 'PhabricatorDashboardProfileController',
'PhabricatorDataCacheSpec' => 'PhabricatorCacheSpec',
'PhabricatorDataNotAttachedException' => 'Exception',
- 'PhabricatorDatabaseHealthRecord' => 'Phobject',
'PhabricatorDatabaseRef' => 'Phobject',
'PhabricatorDatabaseRefParser' => 'Phobject',
'PhabricatorDatabaseSetupCheck' => 'PhabricatorSetupCheck',
@@ -7738,6 +7748,7 @@
'PhabricatorEditorMultipleSetting' => 'PhabricatorSelectSetting',
'PhabricatorEditorSetting' => 'PhabricatorStringSetting',
'PhabricatorElasticFulltextStorageEngine' => 'PhabricatorFulltextStorageEngine',
+ 'PhabricatorElasticSearchHost' => 'PhabricatorSearchHost',
'PhabricatorElasticSearchSetupCheck' => 'PhabricatorSetupCheck',
'PhabricatorEmailAddressesSettingsPanel' => 'PhabricatorSettingsPanel',
'PhabricatorEmailContentSource' => 'PhabricatorContentSource',
@@ -8208,6 +8219,7 @@
'PhabricatorMySQLConfigOptions' => 'PhabricatorApplicationConfigOptions',
'PhabricatorMySQLFileStorageEngine' => 'PhabricatorFileStorageEngine',
'PhabricatorMySQLFulltextStorageEngine' => 'PhabricatorFulltextStorageEngine',
+ 'PhabricatorMySQLSearchHost' => 'PhabricatorSearchHost',
'PhabricatorMySQLSetupCheck' => 'PhabricatorSetupCheck',
'PhabricatorNamedQuery' => array(
'PhabricatorSearchDAO',
@@ -9074,7 +9086,6 @@
'PhabricatorSearchApplicationStorageEnginePanel' => 'PhabricatorApplicationConfigurationPanel',
'PhabricatorSearchBaseController' => 'PhabricatorController',
'PhabricatorSearchCheckboxesField' => 'PhabricatorSearchField',
- 'PhabricatorSearchConfigOptions' => 'PhabricatorApplicationConfigOptions',
'PhabricatorSearchConstraintException' => 'Exception',
'PhabricatorSearchController' => 'PhabricatorSearchBaseController',
'PhabricatorSearchCustomFieldProxyField' => 'PhabricatorSearchField',
@@ -9097,6 +9108,7 @@
'PhabricatorSearchEngineExtensionModule' => 'PhabricatorConfigModule',
'PhabricatorSearchEngineTestCase' => 'PhabricatorTestCase',
'PhabricatorSearchField' => 'Phobject',
+ 'PhabricatorSearchHost' => 'Phobject',
'PhabricatorSearchHovercardController' => 'PhabricatorSearchBaseController',
'PhabricatorSearchIndexVersion' => 'PhabricatorSearchDAO',
'PhabricatorSearchIndexVersionDestructionEngineExtension' => 'PhabricatorDestructionEngineExtension',
@@ -9116,6 +9128,7 @@
'PhabricatorSearchSchemaSpec' => 'PhabricatorConfigSchemaSpec',
'PhabricatorSearchScopeSetting' => 'PhabricatorInternalSetting',
'PhabricatorSearchSelectField' => 'PhabricatorSearchField',
+ 'PhabricatorSearchService' => 'Phobject',
'PhabricatorSearchStringListField' => 'PhabricatorSearchField',
'PhabricatorSearchSubscribersField' => 'PhabricatorSearchTokenizerField',
'PhabricatorSearchTextField' => 'PhabricatorSearchField',
diff --git a/src/applications/config/application/PhabricatorConfigApplication.php b/src/applications/config/application/PhabricatorConfigApplication.php
--- a/src/applications/config/application/PhabricatorConfigApplication.php
+++ b/src/applications/config/application/PhabricatorConfigApplication.php
@@ -69,6 +69,7 @@
'databases/' => 'PhabricatorConfigClusterDatabasesController',
'notifications/' => 'PhabricatorConfigClusterNotificationsController',
'repositories/' => 'PhabricatorConfigClusterRepositoriesController',
+ 'search/' => 'PhabricatorConfigClusterSearchController',
),
),
);
diff --git a/src/applications/config/check/PhabricatorElasticSearchSetupCheck.php b/src/applications/config/check/PhabricatorElasticSearchSetupCheck.php
--- a/src/applications/config/check/PhabricatorElasticSearchSetupCheck.php
+++ b/src/applications/config/check/PhabricatorElasticSearchSetupCheck.php
@@ -7,71 +7,74 @@
}
protected function executeChecks() {
- if (!$this->shouldUseElasticSearchEngine()) {
- return;
- }
-
- $engine = new PhabricatorElasticFulltextStorageEngine();
+ $services = PhabricatorSearchService::getAllServices();
- $index_exists = null;
- $index_sane = null;
- try {
- $index_exists = $engine->indexExists();
- if ($index_exists) {
- $index_sane = $engine->indexIsSane();
+ foreach ($services as $service) {
+ try {
+ $host = $service->getAnyHostForRole('read');
+ } catch (PhabricatorClusterNoHostForRoleException $e) {
+ // ignore the error
+ continue;
}
- } catch (Exception $ex) {
- $summary = pht('Elasticsearch is not reachable as configured.');
- $message = pht(
- 'Elasticsearch is configured (with the %s setting) but Phabricator '.
- 'encountered an exception when trying to test the index.'.
- "\n\n".
- '%s',
- phutil_tag('tt', array(), 'search.elastic.host'),
- phutil_tag('pre', array(), $ex->getMessage()));
+ if ($host instanceof PhabricatorElasticSearchHost) {
+ $index_exists = null;
+ $index_sane = null;
+ try {
+ $engine = $host->getEngine();
+ $index_exists = $engine->indexExists();
+ if ($index_exists) {
+ $index_sane = $engine->indexIsSane();
+ }
+ } catch (Exception $ex) {
+ $summary = pht('Elasticsearch is not reachable as configured.');
+ $message = pht(
+ 'Elasticsearch is configured (with the %s setting) but Phabricator'.
+ ' encountered an exception when trying to test the index.'.
+ "\n\n".
+ '%s',
+ phutil_tag('tt', array(), 'cluster.search'),
+ phutil_tag('pre', array(), $ex->getMessage()));
- $this->newIssue('elastic.misconfigured')
- ->setName(pht('Elasticsearch Misconfigured'))
- ->setSummary($summary)
- ->setMessage($message)
- ->addRelatedPhabricatorConfig('search.elastic.host');
- return;
- }
+ $this->newIssue('elastic.misconfigured')
+ ->setName(pht('Elasticsearch Misconfigured'))
+ ->setSummary($summary)
+ ->setMessage($message)
+ ->addRelatedPhabricatorConfig('cluster.search');
+ return;
+ }
- if (!$index_exists) {
- $summary = pht(
- 'You enabled Elasticsearch but the index does not exist.');
+ if (!$index_exists) {
+ $summary = pht(
+ 'You enabled Elasticsearch but the index does not exist.');
- $message = pht(
- 'You likely enabled search.elastic.host without creating the '.
- 'index. Run `./bin/search init` to correct the index.');
+ $message = pht(
+ 'You likely enabled cluster.search without creating the '.
+ 'index. Run `./bin/search init` to correct the index.');
- $this
- ->newIssue('elastic.missing-index')
- ->setName(pht('Elasticsearch index Not Found'))
- ->setSummary($summary)
- ->setMessage($message)
- ->addRelatedPhabricatorConfig('search.elastic.host');
- } else if (!$index_sane) {
- $summary = pht(
- 'Elasticsearch index exists but needs correction.');
+ $this
+ ->newIssue('elastic.missing-index')
+ ->setName(pht('Elasticsearch index Not Found'))
+ ->setSummary($summary)
+ ->setMessage($message)
+ ->addRelatedPhabricatorConfig('cluster.search');
+ } else if (!$index_sane) {
+ $summary = pht(
+ 'Elasticsearch index exists but needs correction.');
- $message = pht(
- 'Either the Phabricator schema for Elasticsearch has changed '.
- 'or Elasticsearch created the index automatically. Run '.
- '`./bin/search init` to correct the index.');
+ $message = pht(
+ 'Either the Phabricator schema for Elasticsearch has changed '.
+ 'or Elasticsearch created the index automatically. Run '.
+ '`./bin/search init` to correct the index.');
- $this
- ->newIssue('elastic.broken-index')
- ->setName(pht('Elasticsearch index Incorrect'))
- ->setSummary($summary)
- ->setMessage($message);
+ $this
+ ->newIssue('elastic.broken-index')
+ ->setName(pht('Elasticsearch index Incorrect'))
+ ->setSummary($summary)
+ ->setMessage($message);
+ }
+ }
}
}
- protected function shouldUseElasticSearchEngine() {
- $search_engine = PhabricatorFulltextStorageEngine::loadEngine();
- return ($search_engine instanceof PhabricatorElasticFulltextStorageEngine);
- }
}
diff --git a/src/applications/config/check/PhabricatorExtraConfigSetupCheck.php b/src/applications/config/check/PhabricatorExtraConfigSetupCheck.php
--- a/src/applications/config/check/PhabricatorExtraConfigSetupCheck.php
+++ b/src/applications/config/check/PhabricatorExtraConfigSetupCheck.php
@@ -198,6 +198,10 @@
'This option has been removed, you can use Dashboards to provide '.
'homepage customization. See T11533 for more details.');
+ $elastic_reason = pht(
+ 'Elasticsearch is now configured with "%s".',
+ 'cluster.search');
+
$ancient_config += array(
'phid.external-loaders' =>
pht(
@@ -348,6 +352,10 @@
'mysql.configuration-provider' => pht(
'Phabricator now has application-level management of partitioning '.
'and replicas.'),
+
+ 'search.elastic.host' => $elastic_reason,
+ 'search.elastic.namespace' => $elastic_reason,
+
);
return $ancient_config;
diff --git a/src/applications/config/check/PhabricatorMySQLSetupCheck.php b/src/applications/config/check/PhabricatorMySQLSetupCheck.php
--- a/src/applications/config/check/PhabricatorMySQLSetupCheck.php
+++ b/src/applications/config/check/PhabricatorMySQLSetupCheck.php
@@ -379,8 +379,13 @@
}
protected function shouldUseMySQLSearchEngine() {
- $search_engine = PhabricatorFulltextStorageEngine::loadEngine();
- return ($search_engine instanceof PhabricatorMySQLFulltextStorageEngine);
+ $services = PhabricatorSearchService::getAllServices();
+ foreach ($services as $service) {
+ if ($service instanceof PhabricatorMySQLSearchHost) {
+ return true;
+ }
+ }
+ return false;
}
}
diff --git a/src/applications/config/controller/PhabricatorConfigClusterSearchController.php b/src/applications/config/controller/PhabricatorConfigClusterSearchController.php
new file mode 100644
--- /dev/null
+++ b/src/applications/config/controller/PhabricatorConfigClusterSearchController.php
@@ -0,0 +1,129 @@
+<?php
+
+final class PhabricatorConfigClusterSearchController
+ extends PhabricatorConfigController {
+
+ public function handleRequest(AphrontRequest $request) {
+ $nav = $this->buildSideNavView();
+ $nav->selectFilter('cluster/search/');
+
+ $title = pht('Cluster Search');
+ $doc_href = PhabricatorEnv::getDoclink('Cluster: Search');
+
+ $header = id(new PHUIHeaderView())
+ ->setHeader($title)
+ ->setProfileHeader(true)
+ ->addActionLink(
+ id(new PHUIButtonView())
+ ->setIcon('fa-book')
+ ->setHref($doc_href)
+ ->setTag('a')
+ ->setText(pht('Documentation')));
+
+ $crumbs = $this
+ ->buildApplicationCrumbs($nav)
+ ->addTextCrumb($title)
+ ->setBorder(true);
+
+ $search_status = $this->buildClusterSearchStatus();
+
+ $content = id(new PhabricatorConfigPageView())
+ ->setHeader($header)
+ ->setContent($search_status);
+
+ return $this->newPage()
+ ->setTitle($title)
+ ->setCrumbs($crumbs)
+ ->setNavigation($nav)
+ ->appendChild($content)
+ ->addClass('white-background');
+ }
+
+ private function buildClusterSearchStatus() {
+ $viewer = $this->getViewer();
+
+ $services = PhabricatorSearchService::getAllServices();
+ Javelin::initBehavior('phabricator-tooltips');
+
+ $view = array();
+ foreach ($services as $service) {
+ $view[] = $this->renderStatusView($service);
+ }
+ return $view;
+ }
+
+ private function renderStatusView($service) {
+ $head = array_merge(
+ array(pht('Type')),
+ array_keys($service->getStatusViewColumns()),
+ array(pht('Status')));
+
+ $rows = array();
+
+ $status_map = PhabricatorSearchService::getConnectionStatusMap();
+ $stats = false;
+ $stats_view = false;
+
+ foreach ($service->getHosts() as $host) {
+ try {
+ $status = $host->getConnectionStatus();
+ $status = idx($status_map, $status, array());
+ } catch (Exception $ex) {
+ $status['icon'] = 'fa-times';
+ $status['label'] = pht('Connection Error');
+ $status['color'] = 'red';
+ $host->didHealthCheck(false);
+ }
+
+ if (!$stats_view) {
+ try {
+ $stats = $host->getEngine()->getIndexStats($host);
+ $stats_view = $this->renderIndexStats($stats);
+ } catch (Exception $e) {
+ $stats_view = false;
+ }
+ }
+
+ $type_icon = 'fa-search sky';
+ $type_tip = $host->getDisplayName();
+
+ $type_icon = id(new PHUIIconView())
+ ->setIcon($type_icon);
+ $status_view = array(
+ id(new PHUIIconView())->setIcon($status['icon'].' '.$status['color']),
+ ' ',
+ $status['label'],
+ );
+ $row = array(array($type_icon, ' ', $type_tip));
+ $row = array_merge($row, array_values(
+ $host->getStatusViewColumns()));
+ $row[] = $status_view;
+ $rows[] = $row;
+ }
+
+ $table = id(new AphrontTableView($rows))
+ ->setNoDataString(pht('No search servers are configured.'))
+ ->setHeaders($head);
+
+ $view = id(new PHUIObjectBoxView())
+ ->setHeaderText($service->getDisplayName())
+ ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
+ ->setTable($table);
+
+ if ($stats_view) {
+ $view->addPropertyList($stats_view);
+ }
+ return $view;
+ }
+
+ private function renderIndexStats($stats) {
+ $view = id(new PHUIPropertyListView());
+ if ($stats !== false) {
+ foreach ($stats as $label => $val) {
+ $view->addProperty($label, $val);
+ }
+ }
+ return $view;
+ }
+
+}
diff --git a/src/applications/config/controller/PhabricatorConfigController.php b/src/applications/config/controller/PhabricatorConfigController.php
--- a/src/applications/config/controller/PhabricatorConfigController.php
+++ b/src/applications/config/controller/PhabricatorConfigController.php
@@ -42,8 +42,11 @@
pht('Notification Servers'), null, 'fa-bell-o');
$nav->addFilter('cluster/repositories/',
pht('Repository Servers'), null, 'fa-code');
+ $nav->addFilter('cluster/search/',
+ pht('Search Servers'), null, 'fa-search');
$nav->addLabel(pht('Modules'));
+
$modules = PhabricatorConfigModule::getAllModules();
foreach ($modules as $key => $module) {
$nav->addFilter('module/'.$key.'/',
diff --git a/src/applications/config/option/PhabricatorClusterConfigOptions.php b/src/applications/config/option/PhabricatorClusterConfigOptions.php
--- a/src/applications/config/option/PhabricatorClusterConfigOptions.php
+++ b/src/applications/config/option/PhabricatorClusterConfigOptions.php
@@ -38,6 +38,17 @@
$intro_href = PhabricatorEnv::getDoclink('Clustering Introduction');
$intro_name = pht('Clustering Introduction');
+ $search_type = 'custom:PhabricatorClusterSearchConfigOptionType';
+ $search_help = $this->deformat(pht(<<<EOTEXT
+Define one or more fulltext storage services. Here you can configure which
+hosts will handle fulltext search queries and indexing. For help with
+configuring fulltext search clusters, see **[[ %s | %s ]]** in the
+documentation.
+EOTEXT
+ ,
+ PhabricatorEnv::getDoclink('Cluster: Search'),
+ pht('Cluster: Search')));
+
return array(
$this->newOption('cluster.addresses', 'list<string>', array())
->setLocked(true)
@@ -114,6 +125,21 @@
->setSummary(
pht('Configure database read replicas.'))
->setDescription($databases_help),
+ $this->newOption('cluster.search', $search_type, array())
+ ->setLocked(true)
+ ->setSummary(
+ pht('Configure full-text search services.'))
+ ->setDescription($search_help)
+ ->setDefault(
+ array(
+ array(
+ 'type' => 'mysql',
+ 'roles' => array(
+ 'read' => true,
+ 'write' => true,
+ ),
+ ),
+ )),
);
}
diff --git a/src/applications/maniphest/query/ManiphestTaskQuery.php b/src/applications/maniphest/query/ManiphestTaskQuery.php
--- a/src/applications/maniphest/query/ManiphestTaskQuery.php
+++ b/src/applications/maniphest/query/ManiphestTaskQuery.php
@@ -513,14 +513,14 @@
->setEngineClassName('PhabricatorSearchApplicationSearchEngine')
->setParameter('query', $this->fullTextSearch);
- // NOTE: Setting this to something larger than 2^53 will raise errors in
+ // NOTE: Setting this to something larger than 10,000 will raise errors in
// ElasticSearch, and billions of results won't fit in memory anyway.
- $fulltext_query->setParameter('limit', 100000);
+ $fulltext_query->setParameter('limit', 10000);
$fulltext_query->setParameter('types',
array(ManiphestTaskPHIDType::TYPECONST));
- $engine = PhabricatorFulltextStorageEngine::loadEngine();
- $fulltext_results = $engine->executeSearch($fulltext_query);
+ $fulltext_results = PhabricatorSearchService::executeSearch(
+ $fulltext_query);
if (empty($fulltext_results)) {
$fulltext_results = array(null);
diff --git a/src/applications/project/search/PhabricatorProjectFulltextEngine.php b/src/applications/project/search/PhabricatorProjectFulltextEngine.php
--- a/src/applications/project/search/PhabricatorProjectFulltextEngine.php
+++ b/src/applications/project/search/PhabricatorProjectFulltextEngine.php
@@ -10,7 +10,15 @@
$project = $object;
$project->updateDatasourceTokens();
- $document->setDocumentTitle($project->getName());
+ $document->setDocumentTitle($project->getDisplayName());
+ $document->addField(PhabricatorSearchDocumentFieldType::FIELD_KEYWORDS,
+ $project->getPrimarySlug());
+ try {
+ $slugs = $project->getSlugs();
+ foreach ($slugs as $slug) {}
+ } catch (PhabricatorDataNotAttachedException $e) {
+ // ignore
+ }
$document->addRelationship(
$project->isArchived()
diff --git a/src/applications/search/config/PhabricatorSearchConfigOptions.php b/src/applications/search/config/PhabricatorSearchConfigOptions.php
deleted file mode 100644
--- a/src/applications/search/config/PhabricatorSearchConfigOptions.php
+++ /dev/null
@@ -1,35 +0,0 @@
-<?php
-
-final class PhabricatorSearchConfigOptions
- extends PhabricatorApplicationConfigOptions {
-
- public function getName() {
- return pht('Search');
- }
-
- public function getDescription() {
- return pht('Options relating to Search.');
- }
-
- public function getIcon() {
- return 'fa-search';
- }
-
- public function getGroup() {
- return 'apps';
- }
-
- public function getOptions() {
- return array(
- $this->newOption('search.elastic.host', 'string', null)
- ->setLocked(true)
- ->setDescription(pht('Elastic Search host.'))
- ->addExample('http://elastic.example.com:9200/', pht('Valid Setting')),
- $this->newOption('search.elastic.namespace', 'string', 'phabricator')
- ->setLocked(true)
- ->setDescription(pht('Elastic Search index.'))
- ->addExample('phabricator2', pht('Valid Setting')),
- );
- }
-
-}
diff --git a/src/applications/search/constants/PhabricatorSearchDocumentFieldType.php b/src/applications/search/constants/PhabricatorSearchDocumentFieldType.php
--- a/src/applications/search/constants/PhabricatorSearchDocumentFieldType.php
+++ b/src/applications/search/constants/PhabricatorSearchDocumentFieldType.php
@@ -5,5 +5,6 @@
const FIELD_TITLE = 'titl';
const FIELD_BODY = 'body';
const FIELD_COMMENT = 'cmnt';
+ const FIELD_KEYWORDS = 'kwrd';
}
diff --git a/src/applications/search/engine/__tests__/PhabricatorSearchEngineTestCase.php b/src/applications/search/engine/__tests__/PhabricatorSearchEngineTestCase.php
--- a/src/applications/search/engine/__tests__/PhabricatorSearchEngineTestCase.php
+++ b/src/applications/search/engine/__tests__/PhabricatorSearchEngineTestCase.php
@@ -3,8 +3,8 @@
final class PhabricatorSearchEngineTestCase extends PhabricatorTestCase {
public function testLoadAllEngines() {
- PhabricatorFulltextStorageEngine::loadAllEngines();
- $this->assertTrue(true);
+ $services = PhabricatorSearchService::getAllServices();
+ $this->assertTrue(!empty($services));
}
}
diff --git a/src/applications/search/fulltextstorage/PhabricatorElasticFulltextStorageEngine.php b/src/applications/search/fulltextstorage/PhabricatorElasticFulltextStorageEngine.php
--- a/src/applications/search/fulltextstorage/PhabricatorElasticFulltextStorageEngine.php
+++ b/src/applications/search/fulltextstorage/PhabricatorElasticFulltextStorageEngine.php
@@ -1,37 +1,52 @@
<?php
-final class PhabricatorElasticFulltextStorageEngine
+class PhabricatorElasticFulltextStorageEngine
extends PhabricatorFulltextStorageEngine {
- private $uri;
private $index;
private $timeout;
-
- public function __construct() {
- $this->uri = PhabricatorEnv::getEnvConfig('search.elastic.host');
- $this->index = PhabricatorEnv::getEnvConfig('search.elastic.namespace');
+ private $version;
+
+ public function setService(PhabricatorSearchService $service) {
+ $this->service = $service;
+ $config = $service->getConfig();
+ $index = idx($config, 'path', '/phabricator');
+ $this->index = str_replace('/', '', $index);
+ $this->timeout = idx($config, 'timeout', 15);
+ $this->version = (int)idx($config, 'version', 5);
+ return $this;
}
public function getEngineIdentifier() {
return 'elasticsearch';
}
- public function getEnginePriority() {
- return 10;
+ public function getTimestampField() {
+ return $this->version < 2 ?
+ '_timestamp' : 'lastModified';
}
- public function isEnabled() {
- return (bool)$this->uri;
+ public function getTextFieldType() {
+ return $this->version >= 5
+ ? 'text' : 'string';
}
- public function setURI($uri) {
- $this->uri = $uri;
- return $this;
+ public function getHostType() {
+ return new PhabricatorElasticSearchHost($this);
}
- public function setIndex($index) {
- $this->index = $index;
- return $this;
+ /**
+ * @return PhabricatorElasticSearchHost
+ */
+ public function getHostForRead() {
+ return $this->getService()->getAnyHostForRole('read');
+ }
+
+ /**
+ * @return PhabricatorElasticSearchHost
+ */
+ public function getHostForWrite() {
+ return $this->getService()->getAnyHostForRole('write');
}
public function setTimeout($timeout) {
@@ -39,21 +54,21 @@
return $this;
}
- public function getURI() {
- return $this->uri;
- }
-
- public function getIndex() {
- return $this->index;
- }
-
public function getTimeout() {
return $this->timeout;
}
+ public function getTypeConstants($class) {
+ $relationship_class = new ReflectionClass($class);
+ $typeconstants = $relationship_class->getConstants();
+ return array_unique(array_values($typeconstants));
+ }
+
public function reindexAbstractDocument(
PhabricatorSearchAbstractDocument $doc) {
+ $host = $this->getHostForWrite();
+
$type = $doc->getDocumentType();
$phid = $doc->getPHID();
$handle = id(new PhabricatorHandleQuery())
@@ -61,36 +76,47 @@
->withPHIDs(array($phid))
->executeOne();
+ $timestamp_key = $this->getTimestampField();
+
// URL is not used internally but it can be useful externally.
$spec = array(
'title' => $doc->getDocumentTitle(),
'url' => PhabricatorEnv::getProductionURI($handle->getURI()),
'dateCreated' => $doc->getDocumentCreated(),
- '_timestamp' => $doc->getDocumentModified(),
- 'field' => array(),
- 'relationship' => array(),
+ $timestamp_key => $doc->getDocumentModified(),
);
foreach ($doc->getFieldData() as $field) {
- $spec['field'][] = array_combine(array('type', 'corpus', 'aux'), $field);
+ list($field_name, $corpus, $aux) = $field;
+ if (!isset($spec[$field_name])) {
+ $spec[$field_name] = array($corpus);
+ } else {
+ $spec[$field_name][] = $corpus;
+ }
+ if ($aux != null) {
+ $spec[$field_name][] = $aux;
+ }
}
- foreach ($doc->getRelationshipData() as $relationship) {
- list($rtype, $to_phid, $to_type, $time) = $relationship;
- $spec['relationship'][$rtype][] = array(
- 'phid' => $to_phid,
- 'phidType' => $to_type,
- 'when' => (int)$time,
- );
+ foreach ($doc->getRelationshipData() as $field) {
+ list($field_name, $related_phid, $rtype, $time) = $field;
+ if (!isset($spec[$field_name])) {
+ $spec[$field_name] = array($related_phid);
+ } else {
+ $spec[$field_name][] = $related_phid;
+ }
+ if ($time) {
+ $spec[$field_name.'_ts'] = $time;
+ }
}
- $this->executeRequest("/{$type}/{$phid}/", $spec, 'PUT');
+ $this->executeRequest($host, "/{$type}/{$phid}/", $spec, 'PUT');
}
public function reconstructDocument($phid) {
$type = phid_get_type($phid);
-
- $response = $this->executeRequest("/{$type}/{$phid}", array());
+ $host = $this->getHostForRead();
+ $response = $this->executeRequest($host, "/{$type}/{$phid}", array());
if (empty($response['exists'])) {
return null;
@@ -103,10 +129,11 @@
$doc->setDocumentType($response['_type']);
$doc->setDocumentTitle($hit['title']);
$doc->setDocumentCreated($hit['dateCreated']);
- $doc->setDocumentModified($hit['_timestamp']);
+ $doc->setDocumentModified($hit[$this->getTimestampField()]);
foreach ($hit['field'] as $fdef) {
- $doc->addField($fdef['type'], $fdef['corpus'], $fdef['aux']);
+ $field_type = $fdef['type'];
+ $doc->addField($field_type, $hit[$field_type], $fdef['aux']);
}
foreach ($hit['relationship'] as $rtype => $rships) {
@@ -123,35 +150,51 @@
}
private function buildSpec(PhabricatorSavedQuery $query) {
- $spec = array();
- $filter = array();
- $title_spec = array();
-
- if (strlen($query->getParameter('query'))) {
- $spec[] = array(
+ $q = new PhabricatorElasticSearchQueryBuilder('bool');
+ $query_string = $query->getParameter('query');
+ if (strlen($query_string)) {
+ $fields = $this->getTypeConstants('PhabricatorSearchDocumentFieldType');
+
+ // Build a simple_query_string query over all fields that must match all
+ // of the words in the search string.
+ $q->addMustClause(array(
'simple_query_string' => array(
- 'query' => $query->getParameter('query'),
- 'fields' => array('field.corpus'),
+ 'query' => $query_string,
+ 'fields' => array(
+ '_all',
+ ),
+ 'default_operator' => 'OR',
),
- );
+ ));
- $title_spec = array(
+ // This second query clause is "SHOULD' so it only affects ranking of
+ // documents which already matched the Must clause. This amplifies the
+ // score of documents which have an exact match on title, body
+ // or comments.
+ $q->addShouldClause(array(
'simple_query_string' => array(
- 'query' => $query->getParameter('query'),
- 'fields' => array('title'),
+ 'query' => $query_string,
+ 'fields' => array(
+ PhabricatorSearchDocumentFieldType::FIELD_TITLE.'^4',
+ PhabricatorSearchDocumentFieldType::FIELD_BODY.'^3',
+ PhabricatorSearchDocumentFieldType::FIELD_COMMENT.'^1.2',
+ ),
+ 'analyzer' => 'english_exact',
+ 'default_operator' => 'and',
),
- );
+ ));
+
}
$exclude = $query->getParameter('exclude');
if ($exclude) {
- $filter[] = array(
+ $q->addFilterClause(array(
'not' => array(
'ids' => array(
'values' => array($exclude),
),
),
- );
+ ));
}
$relationship_map = array(
@@ -176,75 +219,59 @@
$include_closed = !empty($statuses[$rel_closed]);
if ($include_open && !$include_closed) {
- $relationship_map[$rel_open] = true;
+ $q->addExistsClause($rel_open);
} else if (!$include_open && $include_closed) {
- $relationship_map[$rel_closed] = true;
+ $q->addExistsClause($rel_closed);
}
if ($query->getParameter('withUnowned')) {
- $relationship_map[$rel_unowned] = true;
+ $q->addExistsClause($rel_unowned);
}
$rel_owner = PhabricatorSearchRelationship::RELATIONSHIP_OWNER;
if ($query->getParameter('withAnyOwner')) {
- $relationship_map[$rel_owner] = true;
+ $q->addExistsClause($rel_owner);
} else {
$owner_phids = $query->getParameter('ownerPHIDs', array());
- $relationship_map[$rel_owner] = $owner_phids;
- }
-
- foreach ($relationship_map as $field => $param) {
- if (is_array($param) && $param) {
- $should = array();
- foreach ($param as $val) {
- $should[] = array(
- 'match' => array(
- "relationship.{$field}.phid" => array(
- 'query' => $val,
- 'type' => 'phrase',
- ),
- ),
- );
- }
- // We couldn't solve it by minimum_number_should_match because it can
- // match multiple owners without matching author.
- $spec[] = array('bool' => array('should' => $should));
- } else if ($param) {
- $filter[] = array(
- 'exists' => array(
- 'field' => "relationship.{$field}.phid",
- ),
- );
+ if (count($owner_phids)) {
+ $q->addTermsClause($rel_owner, $owner_phids);
}
}
- if ($spec) {
- $spec = array('query' => array('bool' => array('must' => $spec)));
- if ($title_spec) {
- $spec['query']['bool']['should'] = $title_spec;
+ foreach ($relationship_map as $field => $phids) {
+ if (is_array($phids) && !empty($phids)) {
+ $q->addTermsClause($field, $phids);
}
}
- if ($filter) {
- $filter = array('filter' => array('and' => $filter));
- if (!$spec) {
- $spec = array('query' => array('match_all' => new stdClass()));
- }
- $spec = array(
- 'query' => array(
- 'filtered' => $spec + $filter,
- ),
- );
+ if (!$q->getClauseCount('must')) {
+ $q->addMustClause(array('match_all' => array('boost' => 1 )));
}
+ $spec = array(
+ '_source' => false,
+ 'query' => array(
+ 'bool' => $q->toArray(),
+ ),
+ );
+
+
if (!$query->getParameter('query')) {
$spec['sort'] = array(
array('dateCreated' => 'desc'),
);
}
- $spec['from'] = (int)$query->getParameter('offset', 0);
- $spec['size'] = (int)$query->getParameter('limit', 25);
+ $offset = (int)$query->getParameter('offset', 0);
+ $limit = (int)$query->getParameter('limit', 101);
+ if ($offset + $limit > 10000) {
+ throw new Exception(pht(
+ 'Query offset is too large. offset+limit=%s (max=%s)',
+ $offset + $limit,
+ 10000));
+ }
+ $spec['from'] = $offset;
+ $spec['size'] = $limit;
return $spec;
}
@@ -261,30 +288,36 @@
// some bigger index). Use '/$types/_search' instead.
$uri = '/'.implode(',', $types).'/_search';
- try {
- $response = $this->executeRequest($uri, $this->buildSpec($query));
- } catch (HTTPFutureHTTPResponseStatus $ex) {
- // elasticsearch probably uses Lucene query syntax:
- // http://lucene.apache.org/core/3_6_1/queryparsersyntax.html
- // Try literal search if operator search fails.
- if (!strlen($query->getParameter('query'))) {
- throw $ex;
+ $spec = $this->buildSpec($query);
+ $exceptions = array();
+
+ foreach ($this->service->getAllHostsForRole('read') as $host) {
+ try {
+ $response = $this->executeRequest($host, $uri, $spec);
+ $phids = ipull($response['hits']['hits'], '_id');
+ return $phids;
+ } catch (Exception $e) {
+ $exceptions[] = $e;
}
- $query = clone $query;
- $query->setParameter(
- 'query',
- addcslashes(
- $query->getParameter('query'), '+-&|!(){}[]^"~*?:\\'));
- $response = $this->executeRequest($uri, $this->buildSpec($query));
}
-
- $phids = ipull($response['hits']['hits'], '_id');
- return $phids;
+ throw new PhutilAggregateException('All search hosts failed:', $exceptions);
}
- public function indexExists() {
+ public function indexExists(PhabricatorElasticSearchHost $host = null) {
+ if (!$host) {
+ $host = $this->getHostForRead();
+ }
try {
- return (bool)$this->executeRequest('/_status/', array());
+ if ($this->version >= 5) {
+ $uri = '/_stats/';
+ $res = $this->executeRequest($host, $uri, array());
+ return isset($res['indices']['phabricator']);
+ } else if ($this->version >= 2) {
+ $uri = '';
+ } else {
+ $uri = '/_status/';
+ }
+ return (bool)$this->executeRequest($host, $uri, array());
} catch (HTTPFutureHTTPResponseStatus $e) {
if ($e->getStatusCode() == 404) {
return false;
@@ -299,53 +332,85 @@
'index' => array(
'auto_expand_replicas' => '0-2',
'analysis' => array(
- 'filter' => array(
- 'trigrams_filter' => array(
- 'min_gram' => 3,
- 'type' => 'ngram',
- 'max_gram' => 3,
- ),
- ),
'analyzer' => array(
- 'custom_trigrams' => array(
- 'type' => 'custom',
- 'filter' => array(
- 'lowercase',
- 'kstem',
- 'trigrams_filter',
- ),
+ 'english_exact' => array(
'tokenizer' => 'standard',
+ 'filter' => array('lowercase'),
),
),
),
),
);
- $types = array_keys(
+ $fields = $this->getTypeConstants('PhabricatorSearchDocumentFieldType');
+ $relationships = $this->getTypeConstants('PhabricatorSearchRelationship');
+
+ $doc_types = array_keys(
PhabricatorSearchApplicationSearchEngine::getIndexableDocumentTypes());
- foreach ($types as $type) {
- // Use the custom trigram analyzer for the corpus of text
- $data['mappings'][$type]['properties']['field']['properties']['corpus'] =
- array('type' => 'string', 'analyzer' => 'custom_trigrams');
+
+ $text_type = $this->getTextFieldType();
+
+ foreach ($doc_types as $type) {
+ $properties = array();
+ foreach ($fields as $field) {
+ // Use the custom analyzer for the corpus of text
+ $properties[$field] = array(
+ 'type' => $text_type,
+ 'analyzer' => 'english_exact',
+ 'search_analyzer' => 'english',
+ 'search_quote_analyzer' => 'english_exact',
+ );
+ }
+
+ if ($this->version < 5) {
+ foreach ($relationships as $rel) {
+ $properties[$rel] = array(
+ 'type' => 'string',
+ 'index' => 'not_analyzed',
+ 'include_in_all' => false,
+ );
+ $properties[$rel.'_ts'] = array(
+ 'type' => 'date',
+ 'include_in_all' => false,
+ );
+ }
+ } else {
+ foreach ($relationships as $rel) {
+ $properties[$rel] = array(
+ 'type' => 'keyword',
+ 'include_in_all' => false,
+ 'doc_values' => false,
+ );
+ $properties[$rel.'_ts'] = array(
+ 'type' => 'date',
+ 'include_in_all' => false,
+ );
+ }
+ }
// Ensure we have dateCreated since the default query requires it
- $data['mappings'][$type]['properties']['dateCreated']['type'] = 'string';
- }
+ $properties['dateCreated']['type'] = 'date';
+ $properties['lastModified']['type'] = 'date';
+ $data['mappings'][$type]['properties'] = $properties;
+ }
return $data;
}
- public function indexIsSane() {
- if (!$this->indexExists()) {
+ public function indexIsSane(PhabricatorElasticSearchHost $host = null) {
+ if (!$host) {
+ $host = $this->getHostForRead();
+ }
+ if (!$this->indexExists($host)) {
return false;
}
-
- $cur_mapping = $this->executeRequest('/_mapping/', array());
- $cur_settings = $this->executeRequest('/_settings/', array());
+ $cur_mapping = $this->executeRequest($host, '/_mapping/', array());
+ $cur_settings = $this->executeRequest($host, '/_settings/', array());
$actual = array_merge($cur_settings[$this->index],
$cur_mapping[$this->index]);
- return $this->check($actual, $this->getIndexConfiguration());
+ $res = $this->check($actual, $this->getIndexConfiguration());
+ return $res;
}
/**
@@ -355,7 +420,7 @@
* @param $required array
* @return bool
*/
- private function check($actual, $required) {
+ private function check($actual, $required, $path = '') {
foreach ($required as $key => $value) {
if (!array_key_exists($key, $actual)) {
if ($key === '_all') {
@@ -369,7 +434,7 @@
if (!is_array($actual[$key])) {
return false;
}
- if (!$this->check($actual[$key], $value)) {
+ if (!$this->check($actual[$key], $value, $path.'.'.$key)) {
return false;
}
continue;
@@ -403,19 +468,44 @@
}
public function initIndex() {
+ $host = $this->getHostForWrite();
if ($this->indexExists()) {
- $this->executeRequest('/', array(), 'DELETE');
+ $this->executeRequest($host, '/', array(), 'DELETE');
}
$data = $this->getIndexConfiguration();
- $this->executeRequest('/', $data, 'PUT');
+ $this->executeRequest($host, '/', $data, 'PUT');
}
- private function executeRequest($path, array $data, $method = 'GET') {
- $uri = new PhutilURI($this->uri);
- $uri->setPath($this->index);
- $uri->appendPath($path);
- $data = json_encode($data);
+ public function getIndexStats(PhabricatorElasticSearchHost $host = null) {
+ if ($this->version < 2) {
+ return false;
+ }
+ if (!$host) {
+ $host = $this->getHostForRead();
+ }
+ $uri = '/_stats/';
+ $host = $this->getHostForRead();
+
+ $res = $this->executeRequest($host, $uri, array());
+ $stats = $res['indices'][$this->index];
+ return array(
+ pht('Queries') =>
+ idxv($stats, array('primaries', 'search', 'query_total')),
+ pht('Documents') =>
+ idxv($stats, array('total', 'docs', 'count')),
+ pht('Deleted') =>
+ idxv($stats, array('total', 'docs', 'deleted')),
+ pht('Storage Used') =>
+ phutil_format_bytes(idxv($stats,
+ array('total', 'store', 'size_in_bytes'))),
+ );
+ }
+
+ private function executeRequest(PhabricatorElasticSearchHost $host, $path,
+ array $data, $method = 'GET') {
+ $uri = $host->getURI($path);
+ $data = json_encode($data);
$future = new HTTPSFuture($uri, $data);
if ($method != 'GET') {
$future->setMethod($method);
@@ -423,19 +513,30 @@
if ($this->getTimeout()) {
$future->setTimeout($this->getTimeout());
}
- list($body) = $future->resolvex();
+ try {
+ list($body) = $future->resolvex();
+ } catch (HTTPFutureResponseStatus $ex) {
+ if ($ex->isTimeout() || (int)$ex->getStatusCode() > 499) {
+ $host->didHealthCheck(false);
+ }
+ throw $ex;
+ }
if ($method != 'GET') {
return null;
}
try {
- return phutil_json_decode($body);
+ $data = phutil_json_decode($body);
+ $host->didHealthCheck(true);
+ return $data;
} catch (PhutilJSONParserException $ex) {
+ $host->didHealthCheck(false);
throw new PhutilProxyException(
pht('ElasticSearch server returned invalid JSON!'),
$ex);
}
+
}
}
diff --git a/src/applications/search/fulltextstorage/PhabricatorElasticSearchQueryBuilder.php b/src/applications/search/fulltextstorage/PhabricatorElasticSearchQueryBuilder.php
new file mode 100644
--- /dev/null
+++ b/src/applications/search/fulltextstorage/PhabricatorElasticSearchQueryBuilder.php
@@ -0,0 +1,78 @@
+<?php
+
+class PhabricatorElasticSearchQueryBuilder {
+ protected $name;
+ protected $clauses = array();
+
+
+ public function getClauses($termkey = null) {
+ $clauses = $this->clauses;
+ if ($termkey == null) {
+ return $clauses;
+ }
+ if (isset($clauses[$termkey])) {
+ return $clauses[$termkey];
+ }
+ return array();
+ }
+
+ public function getClauseCount($clausekey) {
+ if (isset($this->clauses[$clausekey])) {
+ return count($this->clauses[$clausekey]);
+ } else {
+ return 0;
+ }
+ }
+
+ public function addExistsClause($field) {
+ return $this->addClause('filter', array(
+ 'exists' => array(
+ 'field' => $field,
+ ),
+ ));
+ }
+
+ public function addTermsClause($field, $values) {
+ return $this->addClause('filter', array(
+ 'terms' => array(
+ $field => array_values($values),
+ ),
+ ));
+ }
+
+ public function addMustClause($clause) {
+ return $this->addClause('must', $clause);
+ }
+
+ public function addFilterClause($clause) {
+ return $this->addClause('filter', $clause);
+ }
+
+ public function addShouldClause($clause) {
+ return $this->addClause('should', $clause);
+ }
+
+ public function addMustNotClause($clause) {
+ return $this->addClause('must_not', $clause);
+ }
+
+ public function addClause($clause, $terms) {
+ $this->clauses[$clause][] = $terms;
+ return $this;
+ }
+
+ public function toArray() {
+ $clauses = $this->getClauses();
+ return $clauses;
+ $cleaned = array();
+ foreach ($clauses as $clause => $subclauses) {
+ if (is_array($subclauses) && count($subclauses) == 1) {
+ $cleaned[$clause] = array_shift($subclauses);
+ } else {
+ $cleaned[$clause] = $subclauses;
+ }
+ }
+ return $cleaned;
+ }
+
+}
diff --git a/src/applications/search/fulltextstorage/PhabricatorFulltextStorageEngine.php b/src/applications/search/fulltextstorage/PhabricatorFulltextStorageEngine.php
--- a/src/applications/search/fulltextstorage/PhabricatorFulltextStorageEngine.php
+++ b/src/applications/search/fulltextstorage/PhabricatorFulltextStorageEngine.php
@@ -7,46 +7,40 @@
*/
abstract class PhabricatorFulltextStorageEngine extends Phobject {
-/* -( Engine Metadata )---------------------------------------------------- */
+ protected $service;
+
+ public function getHosts() {
+ return $this->service->getHosts();
+ }
+
+ public function setService(PhabricatorSearchService $service) {
+ $this->service = $service;
+ return $this;
+ }
/**
- * Return a unique, nonempty string which identifies this storage engine.
- *
- * @return string Unique string for this engine, max length 32.
- * @task meta
+ * @return PhabricatorSearchService
*/
- abstract public function getEngineIdentifier();
+ public function getService() {
+ return $this->service;
+ }
/**
- * Prioritize this engine relative to other engines.
- *
- * Engines with a smaller priority number get an opportunity to write files
- * first. Generally, lower-latency filestores should have lower priority
- * numbers, and higher-latency filestores should have higher priority
- * numbers. Setting priority to approximately the number of milliseconds of
- * read latency will generally produce reasonable results.
- *
- * In conjunction with filesize limits, the goal is to store small files like
- * profile images, thumbnails, and text snippets in lower-latency engines,
- * and store large files in higher-capacity engines.
- *
- * @return float Engine priority.
- * @task meta
+ * Implementations must return a prototype host instance which is cloned
+ * by the PhabricatorSearchService infrastructure to configure each engine.
+ * @return PhabricatorSearchHost
*/
- abstract public function getEnginePriority();
+ abstract public function getHostType();
+
+/* -( Engine Metadata )---------------------------------------------------- */
/**
- * Return `true` if the engine is currently writable.
- *
- * Engines that are disabled or missing configuration should return `false`
- * to prevent new writes. If writes were made with this engine in the past,
- * the application may still try to perform reads.
+ * Return a unique, nonempty string which identifies this storage engine.
*
- * @return bool True if this engine can support new writes.
+ * @return string Unique string for this engine, max length 32.
* @task meta
*/
- abstract public function isEnabled();
-
+ abstract public function getEngineIdentifier();
/* -( Managing Documents )------------------------------------------------- */
@@ -84,6 +78,13 @@
abstract public function indexExists();
/**
+ * Implementations should override this method to return a dictionary of
+ * stats which are suitable for display in the admin UI.
+ */
+ abstract public function getIndexStats();
+
+
+ /**
* Is the index in a usable state?
*
* @return bool
@@ -100,39 +101,4 @@
public function initIndex() {}
-/* -( Loading Storage Engines )-------------------------------------------- */
-
- /**
- * @task load
- */
- public static function loadAllEngines() {
- return id(new PhutilClassMapQuery())
- ->setAncestorClass(__CLASS__)
- ->setUniqueMethod('getEngineIdentifier')
- ->setSortMethod('getEnginePriority')
- ->execute();
- }
-
- /**
- * @task load
- */
- public static function loadActiveEngines() {
- $engines = self::loadAllEngines();
-
- $active = array();
- foreach ($engines as $key => $engine) {
- if (!$engine->isEnabled()) {
- continue;
- }
-
- $active[$key] = $engine;
- }
-
- return $active;
- }
-
- public static function loadEngine() {
- return head(self::loadActiveEngines());
- }
-
}
diff --git a/src/applications/search/fulltextstorage/PhabricatorMySQLFulltextStorageEngine.php b/src/applications/search/fulltextstorage/PhabricatorMySQLFulltextStorageEngine.php
--- a/src/applications/search/fulltextstorage/PhabricatorMySQLFulltextStorageEngine.php
+++ b/src/applications/search/fulltextstorage/PhabricatorMySQLFulltextStorageEngine.php
@@ -7,12 +7,8 @@
return 'mysql';
}
- public function getEnginePriority() {
- return 100;
- }
-
- public function isEnabled() {
- return true;
+ public function getHostType() {
+ return new PhabricatorMySQLSearchHost($this);
}
public function reindexAbstractDocument(
@@ -415,4 +411,9 @@
public function indexExists() {
return true;
}
+
+ public function getIndexStats() {
+ return false;
+ }
+
}
diff --git a/src/applications/search/index/PhabricatorFulltextEngine.php b/src/applications/search/index/PhabricatorFulltextEngine.php
--- a/src/applications/search/index/PhabricatorFulltextEngine.php
+++ b/src/applications/search/index/PhabricatorFulltextEngine.php
@@ -40,8 +40,7 @@
$extension->indexFulltextObject($object, $document);
}
- $storage_engine = PhabricatorFulltextStorageEngine::loadEngine();
- $storage_engine->reindexAbstractDocument($document);
+ PhabricatorSearchService::reindexAbstractDocument($document);
}
protected function newAbstractDocument($object) {
diff --git a/src/applications/search/management/PhabricatorSearchManagementInitWorkflow.php b/src/applications/search/management/PhabricatorSearchManagementInitWorkflow.php
--- a/src/applications/search/management/PhabricatorSearchManagementInitWorkflow.php
+++ b/src/applications/search/management/PhabricatorSearchManagementInitWorkflow.php
@@ -13,27 +13,41 @@
public function execute(PhutilArgumentParser $args) {
$console = PhutilConsole::getConsole();
- $engine = PhabricatorFulltextStorageEngine::loadEngine();
-
$work_done = false;
- if (!$engine->indexExists()) {
- $console->writeOut(
- '%s',
- pht('Index does not exist, creating...'));
- $engine->initIndex();
- $console->writeOut(
- "%s\n",
- pht('done.'));
- $work_done = true;
- } else if (!$engine->indexIsSane()) {
- $console->writeOut(
- '%s',
- pht('Index exists but is incorrect, fixing...'));
- $engine->initIndex();
+ foreach (PhabricatorSearchService::getAllServices() as $service) {
$console->writeOut(
"%s\n",
- pht('done.'));
- $work_done = true;
+ pht('Initializing search service "%s"', $service->getDisplayName()));
+
+ try {
+ $host = $service->getAnyHostForRole('write');
+ } catch (PhabricatorClusterNoHostForRoleException $e) {
+ // If there are no writable hosts for a given cluster, skip it
+ $console->writeOut("%s\n", $e->getMessage());
+ continue;
+ }
+
+ $engine = $host->getEngine();
+
+ if (!$engine->indexExists()) {
+ $console->writeOut(
+ '%s',
+ pht('Index does not exist, creating...'));
+ $engine->initIndex();
+ $console->writeOut(
+ "%s\n",
+ pht('done.'));
+ $work_done = true;
+ } else if (!$engine->indexIsSane()) {
+ $console->writeOut(
+ '%s',
+ pht('Index exists but is incorrect, fixing...'));
+ $engine->initIndex();
+ $console->writeOut(
+ "%s\n",
+ pht('done.'));
+ $work_done = true;
+ }
}
if ($work_done) {
diff --git a/src/applications/search/query/PhabricatorSearchDocumentQuery.php b/src/applications/search/query/PhabricatorSearchDocumentQuery.php
--- a/src/applications/search/query/PhabricatorSearchDocumentQuery.php
+++ b/src/applications/search/query/PhabricatorSearchDocumentQuery.php
@@ -73,10 +73,7 @@
$query = id(clone($this->savedQuery))
->setParameter('offset', $this->getOffset())
->setParameter('limit', $this->getRawResultLimit());
-
- $engine = PhabricatorFulltextStorageEngine::loadEngine();
-
- return $engine->executeSearch($query);
+ return PhabricatorSearchService::executeSearch($query);
}
public function getQueryApplicationClass() {
diff --git a/src/docs/user/cluster/cluster_search.diviner b/src/docs/user/cluster/cluster_search.diviner
new file mode 100644
--- /dev/null
+++ b/src/docs/user/cluster/cluster_search.diviner
@@ -0,0 +1,76 @@
+@title Cluster: Search
+@group cluster
+
+Overview
+========
+
+You can configure phabricator to connect to one or more fulltext search clusters
+running either Elasticsearch or MySQL. By default and without further
+configuration, Phabricator will use MySQL for fulltext search. This will be
+adequate for the vast majority of users. Installs with a very large number of
+objects or specialized search needs can consider enabling Elasticsearch for
+better scalability and potentially better search results.
+
+Configuring Search Services
+===========================
+
+To configure an Elasticsearch service, use the `cluster.search` configuration
+option. A typical Elasticsearch configuration will probably look similar to
+the following example:
+
+```lang=json
+{
+ "cluster.search": [
+ {
+ "type": "elasticsearch",
+ "hosts": [
+ {
+ "host": "127.0.0.1",
+ "roles": { "write": true, "read": true }
+ }
+ ],
+ "port": 9200,
+ "protocol": "http",
+ "path": "/phabricator",
+ "version": 5
+ },
+ ],
+}
+```
+
+Supported Options
+-----------------
+| Key | Type |Comments|
+|`type` | String |Engine type. Currently, 'elasticsearch' or 'mysql'|
+|`protocol`| String |Either 'http' or 'https'|
+|`port`| Int |The TCP port that Elasticsearch is bound to|
+|`path`| String |The path portion of the url for phabricator's index.|
+|`version`| Int |The version of Elasticsearch server. Supports either 2 or 5.|
+|`hosts`| List |A list of one or more Elasticsearch host names / addresses.|
+
+Host Configuration
+------------------
+Each search service must have one or more hosts associated with it. Each host
+entry consists of a `host` key, a dictionary of roles and can optionally
+override any of the options that are valid at the service level (see above).
+
+Currently supported roles are `read` and `write`. These can be individually
+enabled or disabled on a per-host basis. A typical setup might include two
+elasticsearch clusters in two separate datacenters. You can configure one
+cluster for reads and both for writes. When one cluster is down for maintenance
+you can simply swap the read role over to the backup cluster and then proceed
+with maintenance without any service interruption.
+
+Monitoring Search Services
+==========================
+
+You can monitor fulltext search in {nav Config > Search Servers}. This interface
+shows you a quick overview of services and their health.
+
+The table on this page shows some basic stats for each configured service,
+followed by the configuration and current status of each host.
+
+NOTE: This page runs its diagnostics //from the web server that is serving the
+request//. If you are recovering from a disaster, the view this page shows
+may be partial or misleading, and two requests served by different servers may
+see different views of the cluster.
diff --git a/src/infrastructure/cluster/PhabricatorDatabaseHealthRecord.php b/src/infrastructure/cluster/PhabricatorClusterServiceHealthRecord.php
rename from src/infrastructure/cluster/PhabricatorDatabaseHealthRecord.php
rename to src/infrastructure/cluster/PhabricatorClusterServiceHealthRecord.php
--- a/src/infrastructure/cluster/PhabricatorDatabaseHealthRecord.php
+++ b/src/infrastructure/cluster/PhabricatorClusterServiceHealthRecord.php
@@ -1,20 +1,19 @@
<?php
-final class PhabricatorDatabaseHealthRecord
+class PhabricatorClusterServiceHealthRecord
extends Phobject {
- private $ref;
+ private $cacheKey;
private $shouldCheck;
private $isHealthy;
private $upEventCount;
private $downEventCount;
- public function __construct(PhabricatorDatabaseRef $ref) {
- $this->ref = $ref;
+ public function __construct($cache_key) {
+ $this->cacheKey = $cache_key;
$this->readState();
}
-
/**
* Is the database currently healthy?
*/
@@ -153,18 +152,13 @@
}
}
- private function getHealthRecordCacheKey() {
- $ref = $this->ref;
-
- $host = $ref->getHost();
- $port = $ref->getPort();
-
- return "cluster.db.health({$host}, {$port})";
+ public function getCacheKey() {
+ return $this->cacheKey;
}
private function readHealthRecord() {
$cache = PhabricatorCaches::getSetupCache();
- $cache_key = $this->getHealthRecordCacheKey();
+ $cache_key = $this->getCacheKey();
$health_record = $cache->getKey($cache_key);
if (!is_array($health_record)) {
@@ -180,7 +174,7 @@
private function writeHealthRecord(array $record) {
$cache = PhabricatorCaches::getSetupCache();
- $cache_key = $this->getHealthRecordCacheKey();
+ $cache_key = $this->getCacheKey();
$cache->setKey($cache_key, $record);
}
diff --git a/src/infrastructure/cluster/PhabricatorDatabaseRef.php b/src/infrastructure/cluster/PhabricatorDatabaseRef.php
--- a/src/infrastructure/cluster/PhabricatorDatabaseRef.php
+++ b/src/infrastructure/cluster/PhabricatorDatabaseRef.php
@@ -14,6 +14,7 @@
const REPLICATION_SLOW = 'replica-slow';
const REPLICATION_NOT_REPLICATING = 'not-replicating';
+ const KEY_HEALTH = 'cluster.db.health';
const KEY_REFS = 'cluster.db.refs';
const KEY_INDIVIDUAL = 'cluster.db.individual';
@@ -489,9 +490,18 @@
return $this;
}
+ private function getHealthRecordCacheKey() {
+ $host = $this->getHost();
+ $port = $this->getPort();
+ $key = self::KEY_HEALTH;
+
+ return "{$key}({$host}, {$port})";
+ }
+
public function getHealthRecord() {
if (!$this->healthRecord) {
- $this->healthRecord = new PhabricatorDatabaseHealthRecord($this);
+ $this->healthRecord = new PhabricatorClusterServiceHealthRecord(
+ $this->getHealthRecordCacheKey());
}
return $this->healthRecord;
}
diff --git a/src/infrastructure/cluster/PhabricatorClusterDatabasesConfigOptionType.php b/src/infrastructure/cluster/config/PhabricatorClusterDatabasesConfigOptionType.php
rename from src/infrastructure/cluster/PhabricatorClusterDatabasesConfigOptionType.php
rename to src/infrastructure/cluster/config/PhabricatorClusterDatabasesConfigOptionType.php
diff --git a/src/infrastructure/cluster/config/PhabricatorClusterSearchConfigOptionType.php b/src/infrastructure/cluster/config/PhabricatorClusterSearchConfigOptionType.php
new file mode 100644
--- /dev/null
+++ b/src/infrastructure/cluster/config/PhabricatorClusterSearchConfigOptionType.php
@@ -0,0 +1,79 @@
+<?php
+
+final class PhabricatorClusterSearchConfigOptionType
+ extends PhabricatorConfigJSONOptionType {
+
+ public function validateOption(PhabricatorConfigOption $option, $value) {
+ if (!is_array($value)) {
+ throw new Exception(
+ pht(
+ 'Search cluster configuration is not valid: value must be a '.
+ 'list of search hosts.'));
+ }
+
+ $engines = PhabricatorSearchService::loadAllFulltextStorageEngines();
+
+ foreach ($value as $index => $spec) {
+ if (!is_array($spec)) {
+ throw new Exception(
+ pht(
+ 'Search cluster configuration is not valid: each entry in the '.
+ 'list must be a dictionary describing a search service, but '.
+ 'the value with index "%s" is not a dictionary.',
+ $index));
+ }
+
+ try {
+ PhutilTypeSpec::checkMap(
+ $spec,
+ array(
+ 'type' => 'string',
+ 'hosts' => 'optional list<map<string, wild>>',
+ 'roles' => 'optional map<string, wild>',
+ 'port' => 'optional int',
+ 'protocol' => 'optional string',
+ 'path' => 'optional string',
+ 'version' => 'optional int',
+ ));
+ } catch (Exception $ex) {
+ throw new Exception(
+ pht(
+ 'Search engine configuration has an invalid service '.
+ 'specification (at index "%s"): %s.',
+ $index,
+ $ex->getMessage()));
+ }
+
+ if (!array_key_exists($spec['type'], $engines)) {
+ throw new Exception(
+ pht('Invalid search engine type: %s. Valid types include: %s',
+ $spec['type'],
+ implode(', ', array_keys($engines))));
+ }
+
+ if (isset($spec['hosts'])) {
+ foreach ($spec['hosts'] as $hostindex => $host) {
+ try {
+ PhutilTypeSpec::checkMap(
+ $host,
+ array(
+ 'host' => 'string',
+ 'roles' => 'optional map<string, wild>',
+ 'port' => 'optional int',
+ 'protocol' => 'optional string',
+ 'path' => 'optional string',
+ 'version' => 'optional int',
+ ));
+ } catch (Exception $ex) {
+ throw new Exception(
+ pht(
+ 'Search cluster configuration has an invalid host '.
+ 'specification (at index "%s"): %s.',
+ $hostindex,
+ $ex->getMessage()));
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/src/infrastructure/cluster/PhabricatorClusterException.php b/src/infrastructure/cluster/exception/PhabricatorClusterException.php
rename from src/infrastructure/cluster/PhabricatorClusterException.php
rename to src/infrastructure/cluster/exception/PhabricatorClusterException.php
diff --git a/src/infrastructure/cluster/PhabricatorClusterExceptionHandler.php b/src/infrastructure/cluster/exception/PhabricatorClusterExceptionHandler.php
rename from src/infrastructure/cluster/PhabricatorClusterExceptionHandler.php
rename to src/infrastructure/cluster/exception/PhabricatorClusterExceptionHandler.php
diff --git a/src/infrastructure/cluster/PhabricatorClusterImpossibleWriteException.php b/src/infrastructure/cluster/exception/PhabricatorClusterImpossibleWriteException.php
rename from src/infrastructure/cluster/PhabricatorClusterImpossibleWriteException.php
rename to src/infrastructure/cluster/exception/PhabricatorClusterImpossibleWriteException.php
diff --git a/src/infrastructure/cluster/PhabricatorClusterImproperWriteException.php b/src/infrastructure/cluster/exception/PhabricatorClusterImproperWriteException.php
rename from src/infrastructure/cluster/PhabricatorClusterImproperWriteException.php
rename to src/infrastructure/cluster/exception/PhabricatorClusterImproperWriteException.php
diff --git a/src/infrastructure/cluster/exception/PhabricatorClusterNoHostForRoleException.php b/src/infrastructure/cluster/exception/PhabricatorClusterNoHostForRoleException.php
new file mode 100644
--- /dev/null
+++ b/src/infrastructure/cluster/exception/PhabricatorClusterNoHostForRoleException.php
@@ -0,0 +1,10 @@
+<?php
+
+final class PhabricatorClusterNoHostForRoleException
+ extends Exception {
+
+ public function __construct($role) {
+ parent::__construct(pht('Search cluster has no hosts for role "%s".',
+ $role));
+ }
+}
diff --git a/src/infrastructure/cluster/PhabricatorClusterStrandedException.php b/src/infrastructure/cluster/exception/PhabricatorClusterStrandedException.php
rename from src/infrastructure/cluster/PhabricatorClusterStrandedException.php
rename to src/infrastructure/cluster/exception/PhabricatorClusterStrandedException.php
diff --git a/src/infrastructure/cluster/search/PhabricatorElasticSearchHost.php b/src/infrastructure/cluster/search/PhabricatorElasticSearchHost.php
new file mode 100644
--- /dev/null
+++ b/src/infrastructure/cluster/search/PhabricatorElasticSearchHost.php
@@ -0,0 +1,82 @@
+<?php
+
+final class PhabricatorElasticSearchHost
+ extends PhabricatorSearchHost {
+
+ private $version = 5;
+ private $path = 'phabricator/';
+ private $protocol = 'http';
+
+ const KEY_REFS = 'search.elastic.refs';
+
+
+ public function setConfig($config) {
+ $this->setRoles(idx($config, 'roles', $this->getRoles()))
+ ->setHost(idx($config, 'host', $this->host))
+ ->setPort(idx($config, 'port', $this->port))
+ ->setProtocol(idx($config, 'protocol', $this->protocol))
+ ->setPath(idx($config, 'path', $this->path))
+ ->setVersion(idx($config, 'version', $this->version));
+ return $this;
+ }
+
+ public function getDisplayName() {
+ return pht('ElasticSearch');
+ }
+
+ public function getStatusViewColumns() {
+ return array(
+ pht('Protocol') => $this->getProtocol(),
+ pht('Host') => $this->getHost(),
+ pht('Port') => $this->getPort(),
+ pht('Index Path') => $this->getPath(),
+ pht('Elastic Version') => $this->getVersion(),
+ pht('Roles') => implode(', ', array_keys($this->getRoles())),
+ );
+ }
+
+ public function setProtocol($protocol) {
+ $this->protocol = $protocol;
+ return $this;
+ }
+
+ public function getProtocol() {
+ return $this->protocol;
+ }
+
+ public function setPath($path) {
+ $this->path = $path;
+ return $this;
+ }
+
+ public function getPath() {
+ return $this->path;
+ }
+
+ public function setVersion($version) {
+ $this->version = $version;
+ return $this;
+ }
+
+ public function getVersion() {
+ return $this->version;
+ }
+
+ public function getURI($to_path = null) {
+ $uri = id(new PhutilURI('http://'.$this->getHost()))
+ ->setProtocol($this->getProtocol())
+ ->setPort($this->getPort())
+ ->setPath($this->getPath());
+
+ if ($to_path) {
+ $uri->appendPath($to_path);
+ }
+ return $uri;
+ }
+
+ public function getConnectionStatus() {
+ $status = $this->getEngine()->indexIsSane($this);
+ return $status ? parent::STATUS_OKAY : parent::STATUS_FAIL;
+ }
+
+}
diff --git a/src/infrastructure/cluster/search/PhabricatorMySQLSearchHost.php b/src/infrastructure/cluster/search/PhabricatorMySQLSearchHost.php
new file mode 100644
--- /dev/null
+++ b/src/infrastructure/cluster/search/PhabricatorMySQLSearchHost.php
@@ -0,0 +1,34 @@
+<?php
+
+final class PhabricatorMySQLSearchHost
+ extends PhabricatorSearchHost {
+
+ public function setConfig($config) {
+ $this->setRoles(idx($config, 'roles',
+ array('read' => true, 'write' => true)));
+ return $this;
+ }
+
+ public function getDisplayName() {
+ return 'MySQL';
+ }
+
+ public function getStatusViewColumns() {
+ return array(
+ pht('Protocol') => 'mysql',
+ pht('Roles') => implode(', ', array_keys($this->getRoles())),
+ );
+ }
+
+ public function getProtocol() {
+ return 'mysql';
+ }
+
+ public function getConnectionStatus() {
+ PhabricatorDatabaseRef::queryAll();
+ $ref = PhabricatorDatabaseRef::getMasterDatabaseRefForApplication('search');
+ $status = $ref->getConnectionStatus();
+ return $status;
+ }
+
+}
diff --git a/src/infrastructure/cluster/search/PhabricatorSearchHost.php b/src/infrastructure/cluster/search/PhabricatorSearchHost.php
new file mode 100644
--- /dev/null
+++ b/src/infrastructure/cluster/search/PhabricatorSearchHost.php
@@ -0,0 +1,163 @@
+<?php
+
+abstract class PhabricatorSearchHost
+ extends Phobject {
+
+ const KEY_REFS = 'cluster.search.refs';
+ const KEY_HEALTH = 'cluster.search.health';
+
+ protected $engine;
+ protected $healthRecord;
+ protected $roles = array();
+
+ protected $disabled;
+ protected $host;
+ protected $port;
+ protected $hostRefs = array();
+
+ const STATUS_OKAY = 'okay';
+ const STATUS_FAIL = 'fail';
+
+ public function __construct(PhabricatorFulltextStorageEngine $engine) {
+ $this->engine = $engine;
+ }
+
+ public function setDisabled($disabled) {
+ $this->disabled = $disabled;
+ return $this;
+ }
+
+ public function getDisabled() {
+ return $this->disabled;
+ }
+
+ /**
+ * @return PhabricatorFulltextStorageEngine
+ */
+ public function getEngine() {
+ return $this->engine;
+ }
+
+ public function isWritable() {
+ return $this->hasRole('write');
+ }
+
+ public function isReadable() {
+ return $this->hasRole('read');
+ }
+
+ public function hasRole($role) {
+ return isset($this->roles[$role]) && $this->roles[$role] === true;
+ }
+
+ public function setRoles(array $roles) {
+ foreach ($roles as $role => $val) {
+ $this->roles[$role] = $val;
+ }
+ return $this;
+ }
+
+ public function getRoles() {
+ $roles = array();
+ foreach ($this->roles as $key => $val) {
+ if ($val) {
+ $roles[$key] = $val;
+ }
+ }
+ return $roles;
+ }
+
+ public function setPort($value) {
+ $this->port = $value;
+ return $this;
+ }
+
+ public function getPort() {
+ return $this->port;
+ }
+
+ public function setHost($value) {
+ $this->host = $value;
+ return $this;
+ }
+
+ public function getHost() {
+ return $this->host;
+ }
+
+
+ public function getHealthRecordCacheKey() {
+ $host = $this->getHost();
+ $port = $this->getPort();
+ $key = self::KEY_HEALTH;
+
+ return "{$key}({$host}, {$port})";
+ }
+
+/**
+ * @return PhabricatorClusterServiceHealthRecord
+ */
+ public function getHealthRecord() {
+ if (!$this->healthRecord) {
+ $this->healthRecord = new PhabricatorClusterServiceHealthRecord(
+ $this->getHealthRecordCacheKey());
+ }
+ return $this->healthRecord;
+ }
+
+ public function didHealthCheck($reachable) {
+ $record = $this->getHealthRecord();
+ $should_check = $record->getShouldCheck();
+
+ if ($should_check) {
+ $record->didHealthCheck($reachable);
+ }
+ }
+
+ /**
+ * @return string[] Get a list of fields to show in the status overview UI
+ */
+ abstract public function getStatusViewColumns();
+
+ abstract public function getConnectionStatus();
+
+ public static function reindexAbstractDocument(
+ PhabricatorSearchAbstractDocument $doc) {
+
+ $services = self::getAllServices();
+ $indexed = 0;
+ foreach (self::getWritableHostForEachService() as $host) {
+ $host->getEngine()->reindexAbstractDocument($doc);
+ $indexed++;
+ }
+ if ($indexed == 0) {
+ throw new PhabricatorClusterNoHostForRoleException('write');
+ }
+ }
+
+ public static function executeSearch(PhabricatorSavedQuery $query) {
+ $services = self::getAllServices();
+ foreach ($services as $service) {
+ $hosts = $service->getAllHostsForRole('read');
+ // try all hosts until one succeeds
+ foreach ($hosts as $host) {
+ $last_exception = null;
+ try {
+ $res = $host->getEngine()->executeSearch($query);
+ // return immediately if we get results without an exception
+ $host->didHealthCheck(true);
+ return $res;
+ } catch (Exception $ex) {
+ // try each server in turn, only throw if none succeed
+ $last_exception = $ex;
+ $host->didHealthCheck(false);
+ }
+ }
+ }
+ if ($last_exception) {
+ throw $last_exception;
+ }
+ return $res;
+ }
+
+}
diff --git a/src/infrastructure/cluster/search/PhabricatorSearchService.php b/src/infrastructure/cluster/search/PhabricatorSearchService.php
new file mode 100644
--- /dev/null
+++ b/src/infrastructure/cluster/search/PhabricatorSearchService.php
@@ -0,0 +1,259 @@
+<?php
+
+class PhabricatorSearchService
+ extends Phobject {
+
+ const KEY_REFS = 'cluster.search.refs';
+
+ protected $config;
+ protected $disabled;
+ protected $engine;
+ protected $hosts = array();
+ protected $hostsConfig;
+ protected $hostType;
+ protected $roles = array();
+
+ const STATUS_OKAY = 'okay';
+ const STATUS_FAIL = 'fail';
+
+ public function __construct(PhabricatorFulltextStorageEngine $engine) {
+ $this->engine = $engine;
+ $this->hostType = $engine->getHostType();
+ }
+
+ /**
+ * @throws Exception
+ */
+ public function newHost($config) {
+ $host = clone($this->hostType);
+ $host_config = $this->config + $config;
+ $host->setConfig($host_config);
+ $this->hosts[] = $host;
+ return $host;
+ }
+
+ public function getEngine() {
+ return $this->engine;
+ }
+
+ public function getDisplayName() {
+ return $this->hostType->getDisplayName();
+ }
+
+ public function getStatusViewColumns() {
+ return $this->hostType->getStatusViewColumns();
+ }
+
+ public function setConfig($config) {
+ $this->config = $config;
+
+ if (!isset($config['hosts'])) {
+ $config['hosts'] = array(
+ array(
+ 'host' => idx($config, 'host'),
+ 'port' => idx($config, 'port'),
+ 'protocol' => idx($config, 'protocol'),
+ 'roles' => idx($config, 'roles'),
+ ),
+ );
+ }
+ foreach ($config['hosts'] as $host) {
+ $this->newHost($host);
+ }
+
+ }
+
+ public function getConfig() {
+ return $this->config;
+ }
+
+ public function setDisabled($disabled) {
+ $this->disabled = $disabled;
+ return $this;
+ }
+
+ public function getDisabled() {
+ return $this->disabled;
+ }
+
+ public static function getConnectionStatusMap() {
+ return array(
+ self::STATUS_OKAY => array(
+ 'icon' => 'fa-exchange',
+ 'color' => 'green',
+ 'label' => pht('Okay'),
+ ),
+ self::STATUS_FAIL => array(
+ 'icon' => 'fa-times',
+ 'color' => 'red',
+ 'label' => pht('Failed'),
+ ),
+ );
+ }
+
+ public function isWritable() {
+ return $this->hasRole('write');
+ }
+
+ public function isReadable() {
+ return $this->hasRole('read');
+ }
+
+ public function hasRole($role) {
+ return isset($this->roles[$role]) && $this->roles[$role] === true;
+ }
+
+ public function setRoles(array $roles) {
+ foreach ($roles as $role => $val) {
+ if ($val === false && isset($this->roles[$role])) {
+ unset($this->roles[$role]);
+ } else {
+ $this->roles[$role] = $val;
+ }
+ }
+ return $this;
+ }
+
+ public function getRoles() {
+ return $this->roles;
+ }
+
+ public function getPort() {
+ return idx($this->config, 'port');
+ }
+
+ public function getProtocol() {
+ return idx($this->config, 'protocol');
+ }
+
+
+ public function getVersion() {
+ return idx($this->config, 'version');
+ }
+
+ public function getHosts() {
+ return $this->hosts;
+ }
+
+
+ /**
+ * Get a random host reference with the specified role, skipping hosts which
+ * failed recent health checks.
+ * @throws PhabricatorClusterNoHostForRoleException if no healthy hosts match.
+ * @return PhabricatorSearchHost
+ */
+ public function getAnyHostForRole($role) {
+ $hosts = $this->getAllHostsForRole($role);
+ shuffle($hosts);
+ foreach ($hosts as $host) {
+ $health = $host->getHealthRecord();
+ if ($health->getIsHealthy()) {
+ return $host;
+ }
+ }
+ throw new PhabricatorClusterNoHostForRoleException($role);
+ }
+
+
+ /**
+ * Get all configured hosts for this service which have the specified role.
+ * @return PhabricatorSearchHost[]
+ */
+ public function getAllHostsForRole($role) {
+ $hosts = array();
+ foreach ($this->hosts as $host) {
+ if ($host->hasRole($role)) {
+ $hosts[] = $host;
+ }
+ }
+ return $hosts;
+ }
+
+ /**
+ * Get a reference to all configured fulltext search cluster services
+ * @return PhabricatorSearchService[]
+ */
+ public static function getAllServices() {
+ $cache = PhabricatorCaches::getRequestCache();
+
+ $refs = $cache->getKey(self::KEY_REFS);
+ if (!$refs) {
+ $refs = self::newRefs();
+ $cache->setKey(self::KEY_REFS, $refs);
+ }
+
+ return $refs;
+ }
+
+ /**
+ * Load all valid PhabricatorFulltextStorageEngine subclasses
+ */
+ public static function loadAllFulltextStorageEngines() {
+ return id(new PhutilClassMapQuery())
+ ->setAncestorClass('PhabricatorFulltextStorageEngine')
+ ->setUniqueMethod('getEngineIdentifier')
+ ->execute();
+ }
+
+ /**
+ * Create instances of PhabricatorSearchService based on configuration
+ * @return PhabricatorSearchService[]
+ */
+ public static function newRefs() {
+ $services = PhabricatorEnv::getEnvConfig('cluster.search');
+ $engines = self::loadAllFulltextStorageEngines();
+ $refs = array();
+
+ foreach ($services as $config) {
+ $engine = $engines[$config['type']];
+ $cluster = new self($engine);
+ $cluster->setConfig($config);
+ $engine->setService($cluster);
+ $refs[] = $cluster;
+ }
+
+ return $refs;
+ }
+
+
+ /**
+ * (re)index the document: attempt to pass the document to all writable
+ * fulltext search hosts
+ * @throws PhabricatorClusterNoHostForRoleException
+ */
+ public static function reindexAbstractDocument(
+ PhabricatorSearchAbstractDocument $doc) {
+ $indexed = 0;
+ foreach (self::getAllServices() as $service) {
+ $service->getEngine()->reindexAbstractDocument($doc);
+ $indexed++;
+ }
+ if ($indexed == 0) {
+ throw new PhabricatorClusterNoHostForRoleException('write');
+ }
+ }
+
+ /**
+ * Execute a full-text query and return a list of PHIDs of matching objects.
+ * @return string[]
+ * @throws PhutilAggregateException
+ */
+ public static function executeSearch(PhabricatorSavedQuery $query) {
+ $services = self::getAllServices();
+ $exceptions = array();
+ foreach ($services as $service) {
+ $engine = $service->getEngine();
+ // try all hosts until one succeeds
+ try {
+ $res = $engine->executeSearch($query);
+ // return immediately if we get results without an exception
+ return $res;
+ } catch (Exception $ex) {
+ $exceptions[] = $ex;
+ }
+ }
+ throw new PhutilAggregateException('All search engines failed:',
+ $exceptions);
+ }
+
+}

File Metadata

Mime Type
text/plain
Expires
Thu, May 23, 10:46 PM (3 w, 2 d ago)
Storage Engine
amazon-s3
Storage Format
Encrypted (AES-256-CBC)
Storage Handle
phabricator/secure/2i/42/5ado6xhj7wrdvexc
Default Alt Text
D17384.id.diff (85 KB)

Event Timeline