Remove product literal strings in "pht()", part 18
[phabricator.git] / src / infrastructure / cluster / search / PhabricatorSearchService.php
blob073f9ffab6b7431688abebe5fcadb94aed5b1538
1 <?php
3 class PhabricatorSearchService
4 extends Phobject {
6 const KEY_REFS = 'cluster.search.refs';
8 protected $config;
9 protected $disabled;
10 protected $engine;
11 protected $hosts = array();
12 protected $hostsConfig;
13 protected $hostType;
14 protected $roles = array();
16 const STATUS_OKAY = 'okay';
17 const STATUS_FAIL = 'fail';
19 const ROLE_WRITE = 'write';
20 const ROLE_READ = 'read';
22 public function __construct(PhabricatorFulltextStorageEngine $engine) {
23 $this->engine = $engine;
24 $this->hostType = $engine->getHostType();
27 /**
28 * @throws Exception
30 public function newHost($config) {
31 $host = clone($this->hostType);
32 $host_config = $this->config + $config;
33 $host->setConfig($host_config);
34 $this->hosts[] = $host;
35 return $host;
38 public function getEngine() {
39 return $this->engine;
42 public function getDisplayName() {
43 return $this->hostType->getDisplayName();
46 public function getStatusViewColumns() {
47 return $this->hostType->getStatusViewColumns();
50 public function setConfig($config) {
51 $this->config = $config;
53 if (!isset($config['hosts'])) {
54 $config['hosts'] = array(
55 array(
56 'host' => idx($config, 'host'),
57 'port' => idx($config, 'port'),
58 'protocol' => idx($config, 'protocol'),
59 'roles' => idx($config, 'roles'),
63 foreach ($config['hosts'] as $host) {
64 $this->newHost($host);
69 public function getConfig() {
70 return $this->config;
73 public static function getConnectionStatusMap() {
74 return array(
75 self::STATUS_OKAY => array(
76 'icon' => 'fa-exchange',
77 'color' => 'green',
78 'label' => pht('Okay'),
80 self::STATUS_FAIL => array(
81 'icon' => 'fa-times',
82 'color' => 'red',
83 'label' => pht('Failed'),
88 public function isWritable() {
89 return (bool)$this->getAllHostsForRole(self::ROLE_WRITE);
92 public function isReadable() {
93 return (bool)$this->getAllHostsForRole(self::ROLE_READ);
96 public function getPort() {
97 return idx($this->config, 'port');
100 public function getProtocol() {
101 return idx($this->config, 'protocol');
105 public function getVersion() {
106 return idx($this->config, 'version');
109 public function getHosts() {
110 return $this->hosts;
115 * Get a random host reference with the specified role, skipping hosts which
116 * failed recent health checks.
117 * @throws PhabricatorClusterNoHostForRoleException if no healthy hosts match.
118 * @return PhabricatorSearchHost
120 public function getAnyHostForRole($role) {
121 $hosts = $this->getAllHostsForRole($role);
122 shuffle($hosts);
123 foreach ($hosts as $host) {
124 $health = $host->getHealthRecord();
125 if ($health->getIsHealthy()) {
126 return $host;
129 throw new PhabricatorClusterNoHostForRoleException($role);
134 * Get all configured hosts for this service which have the specified role.
135 * @return PhabricatorSearchHost[]
137 public function getAllHostsForRole($role) {
138 // if the role is explicitly set to false at the top level, then all hosts
139 // have the role disabled.
140 if (idx($this->config, $role) === false) {
141 return array();
144 $hosts = array();
145 foreach ($this->hosts as $host) {
146 if ($host->hasRole($role)) {
147 $hosts[] = $host;
150 return $hosts;
154 * Get a reference to all configured fulltext search cluster services
155 * @return PhabricatorSearchService[]
157 public static function getAllServices() {
158 $cache = PhabricatorCaches::getRequestCache();
160 $refs = $cache->getKey(self::KEY_REFS);
161 if (!$refs) {
162 $refs = self::newRefs();
163 $cache->setKey(self::KEY_REFS, $refs);
166 return $refs;
170 * Load all valid PhabricatorFulltextStorageEngine subclasses
172 public static function loadAllFulltextStorageEngines() {
173 return id(new PhutilClassMapQuery())
174 ->setAncestorClass('PhabricatorFulltextStorageEngine')
175 ->setUniqueMethod('getEngineIdentifier')
176 ->execute();
180 * Create instances of PhabricatorSearchService based on configuration
181 * @return PhabricatorSearchService[]
183 public static function newRefs() {
184 $services = PhabricatorEnv::getEnvConfig('cluster.search');
185 $engines = self::loadAllFulltextStorageEngines();
186 $refs = array();
188 foreach ($services as $config) {
190 // Normally, we've validated configuration before we get this far, but
191 // make sure we don't fatal if we end up here with a bogus configuration.
192 if (!isset($engines[$config['type']])) {
193 throw new Exception(
194 pht(
195 'Configured search engine type "%s" is unknown. Valid engines '.
196 'are: %s.',
197 $config['type'],
198 implode(', ', array_keys($engines))));
201 $engine = clone($engines[$config['type']]);
202 $cluster = new self($engine);
203 $cluster->setConfig($config);
204 $engine->setService($cluster);
205 $refs[] = $cluster;
208 return $refs;
213 * (re)index the document: attempt to pass the document to all writable
214 * fulltext search hosts
216 public static function reindexAbstractDocument(
217 PhabricatorSearchAbstractDocument $document) {
219 $exceptions = array();
220 foreach (self::getAllServices() as $service) {
221 if (!$service->isWritable()) {
222 continue;
225 $engine = $service->getEngine();
226 try {
227 $engine->reindexAbstractDocument($document);
228 } catch (Exception $ex) {
229 $exceptions[] = $ex;
233 if ($exceptions) {
234 throw new PhutilAggregateException(
235 pht(
236 'Writes to search services failed while reindexing document "%s".',
237 $document->getPHID()),
238 $exceptions);
243 * Execute a full-text query and return a list of PHIDs of matching objects.
244 * @return string[]
245 * @throws PhutilAggregateException
247 public static function executeSearch(PhabricatorSavedQuery $query) {
248 $result_set = self::newResultSet($query);
249 return $result_set->getPHIDs();
252 public static function newResultSet(PhabricatorSavedQuery $query) {
253 $exceptions = array();
254 // try all services until one succeeds
255 foreach (self::getAllServices() as $service) {
256 if (!$service->isReadable()) {
257 continue;
260 try {
261 $engine = $service->getEngine();
262 $phids = $engine->executeSearch($query);
264 return id(new PhabricatorFulltextResultSet())
265 ->setPHIDs($phids)
266 ->setFulltextTokens($engine->getFulltextTokens());
267 } catch (PhutilSearchQueryCompilerSyntaxException $ex) {
268 // If there's a query compilation error, return it directly to the
269 // user: they issued a query with bad syntax.
270 throw $ex;
271 } catch (Exception $ex) {
272 $exceptions[] = $ex;
275 $msg = pht('All of the configured Fulltext Search services failed.');
276 throw new PhutilAggregateException($msg, $exceptions);