Skip to content

Commit

Permalink
support suggest.buildAll and add a plugin to execute queries without …
Browse files Browse the repository at this point in the history
…waiting for Solr's response (#1119)

* support suggest.buildAll
added NoResponseRequest plugin

* formatted code

* register noresponserequest plugin

* fixed plugin registration in test

* handle timeout exception

* simplified event handling

* fixed HTTP fake headers in tests

* fixed test

* renamed plugin

* added integration test

* added assertions

* increase test coverage

* formatted code

* added comments and improved test

* minimal documentation

* fixed code format

* Take connection timeout into account

* Skip example for lack of a default suggester

* respect connection timeout

* throw any exception that is not related to timeouts

* Doc fixes

* fixed coding style

* detect connection errors for non Curl adapters

* get microtime as float

* et the adapter before throwing an exception

* coding style

* Fix unit test

* fixed tests

* Additional test coverage

---------

Co-authored-by: thomascorthals <[email protected]>
  • Loading branch information
mkalkbrenner and thomascorthals authored Jan 10, 2024
1 parent 977d5e1 commit 913c00b
Show file tree
Hide file tree
Showing 33 changed files with 556 additions and 65 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]
### Added
- Option `buildAll` for Suggesters
- NoWaitForResponseRequest Plugin

### Fixed
- PHP 8.2 deprecations for Solarium\QueryType\Server\Collections results

Expand Down
30 changes: 30 additions & 0 deletions docs/plugins.md
Original file line number Diff line number Diff line change
Expand Up @@ -439,6 +439,36 @@ htmlFooter();

```

NoWaitForResponseRequest plugin
-------------------------------

Long-running requests like suggest.buildAll might exceed timeouts. This plugin "tries" to convert the request in a kind of fire-and-forget and doesn't wait for Solr's response. Most reliable if the [cURL client adapter](client-and-adapters.md#curl-adapter) is used.

```php
<?php

require_once __DIR__.'/init.php';
htmlHeader();

// create a client instance
$client = new Solarium\Client($adapter, $eventDispatcher, $config);

// get a suggester query instance and add setting to build all suggesters
$suggester = $client->createSuggester();
$suggester->setBuildAll(true);

// don't wait until all suggesters have been built
$plugin = $client->getPlugin('nowaitforresponserequest');

// this executes the query without waiting for the response
$client->suggester($suggester);

// don't forget to remove the plugin again if you do need the response from further requests
$client->removePlugin($plugin);

htmlFooter();
```

ParallelExecution plugin
------------------------

Expand Down
22 changes: 22 additions & 0 deletions examples/7.9-plugin-nowaitforresponserequest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php

require_once __DIR__.'/init.php';
htmlHeader();

// create a client instance
$client = new Solarium\Client($adapter, $eventDispatcher, $config);

// get a suggester query instance and add setting to build all suggesters
$suggester = $client->createSuggester();
$suggester->setBuildAll(true);

// don't wait until all suggesters have been built
$plugin = $client->getPlugin('nowaitforresponserequest');

// this executes the query without waiting for the response
$client->suggester($suggester);

// don't forget to remove the plugin again if you do need the response from further requests
$client->removePlugin($plugin);

htmlFooter();
1 change: 1 addition & 0 deletions examples/execute_all.php
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@
'7.5.3.2-plugin-bufferedupdate-lite-benchmarks-xml.php', // takes too long for a workflow, can be run manually
'7.5.3.3-plugin-bufferedupdate-benchmarks-json.php', // takes too long for a workflow, can be run manually
'7.5.3.4-plugin-bufferedupdate-lite-benchmarks-json.php', // takes too long for a workflow, can be run manually
'7.9-plugin-nowaitforresponserequest.php', // there is no default suggester included with techproducts
];

// examples that can't be run against this Solr version
Expand Down
1 change: 1 addition & 0 deletions examples/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,7 @@ <h2>Examples</h2>
<li><a href="7.7.1-plugin-minimumscorefilter-grouping.php">7.7.1 Minimum score filter for select queries using grouping</a></li>
</ul>
<li><a href="7.8-plugin-postbigextractrequest.php">7.8 Post Big Extract Requests</a></li>
<li><a href="7.9-plugin-nowaitforresponserequest.php">7.9 No Wait For Response Request</a></li>
</ul>

</ul>
Expand Down
22 changes: 22 additions & 0 deletions src/Component/ComponentTraits/SuggesterTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -135,4 +135,26 @@ public function getReload(): ?bool
{
return $this->getOption('reload');
}

/**
* Set buildAll option.
*
* @param bool $buildAll
*
* @return SuggesterInterface Provides fluent interface
*/
public function setBuildAll(bool $buildAll): SuggesterInterface
{
return $this->setOption('buildAll', $buildAll);
}

/**
* Get buildAll option.
*
* @return bool|null
*/
public function getBuildAll(): ?bool
{
return $this->getOption('buildAll');
}
}
16 changes: 16 additions & 0 deletions src/Component/SuggesterInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -108,4 +108,20 @@ public function setReload(bool $reload): self;
* @return bool|null
*/
public function getReload(): ?bool;

/**
* Set buildAll option.
*
* @param bool $buildAll
*
* @return self Provides fluent interface
*/
public function setBuildAll(bool $buildAll): self;

/**
* Get buildAll option.
*
* @return bool|null
*/
public function getBuildAll(): ?bool;
}
10 changes: 7 additions & 3 deletions src/Core/Client/Adapter/Curl.php
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,10 @@ public function execute(Request $request, Endpoint $endpoint): Response
public function getResponse(\CurlHandle $handle, $httpResponse): Response
{
if (CURLE_OK !== curl_errno($handle)) {
throw new HttpException(sprintf('HTTP request failed, %s', curl_error($handle)));
$errno = curl_errno($handle);
$error = curl_error($handle);
curl_close($handle);
throw new HttpException(sprintf('HTTP request failed, %s', $error), $errno);
}

$httpCode = curl_getinfo($handle, CURLINFO_RESPONSE_CODE);
Expand Down Expand Up @@ -85,7 +88,7 @@ public function createHandle(Request $request, Endpoint $endpoint): \CurlHandle

$handler = curl_init();
curl_setopt($handler, CURLOPT_URL, $uri);
curl_setopt($handler, CURLOPT_RETURNTRANSFER, true);
curl_setopt($handler, CURLOPT_RETURNTRANSFER, $options['return_transfer']);
if (!(\function_exists('ini_get') && ini_get('open_basedir'))) {
curl_setopt($handler, CURLOPT_FOLLOWLOCATION, true);
}
Expand Down Expand Up @@ -211,10 +214,11 @@ protected function init()
*/
protected function createOptions(Request $request, Endpoint $endpoint): array
{
$options = [
$options = $this->options + [
'timeout' => $this->timeout,
'connection_timeout' => $this->connectionTimeout ?? $this->timeout,
'proxy' => $this->proxy,
'return_transfer' => true,
];
foreach ($request->getHeaders() as $headerLine) {
list($header, $value) = explode(':', $headerLine);
Expand Down
7 changes: 7 additions & 0 deletions src/Core/Client/Adapter/TimeoutAwareInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,13 @@ interface TimeoutAwareInterface
*/
public const DEFAULT_TIMEOUT = 5;

/**
* Fast timeout that should be used if the client should not wait for the result.
*
* @see \Solarium\Plugin\NoWaitForResponseRequest
*/
public const FAST_TIMEOUT = 1;

/**
* @param int $timeoutInSeconds
*
Expand Down
2 changes: 2 additions & 0 deletions src/Core/Client/Client.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
use Solarium\Plugin\CustomizeRequest\CustomizeRequest;
use Solarium\Plugin\Loadbalancer\Loadbalancer;
use Solarium\Plugin\MinimumScoreFilter\MinimumScoreFilter;
use Solarium\Plugin\NoWaitForResponseRequest;
use Solarium\Plugin\ParallelExecution\ParallelExecution;
use Solarium\Plugin\PostBigExtractRequest;
use Solarium\Plugin\PostBigRequest;
Expand Down Expand Up @@ -243,6 +244,7 @@ class Client extends Configurable implements ClientInterface
*/
protected $pluginTypes = [
'loadbalancer' => Loadbalancer::class,
'nowaitforresponserequest' => NoWaitForResponseRequest::class,
'postbigrequest' => PostBigRequest::class,
'postbigextractrequest' => PostBigExtractRequest::class,
'customizerequest' => CustomizeRequest::class,
Expand Down
145 changes: 145 additions & 0 deletions src/Plugin/NoWaitForResponseRequest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
<?php

/*
* This file is part of the Solarium package.
*
* For the full copyright and license information, please view the COPYING
* file that was distributed with this source code.
*/

namespace Solarium\Plugin;

use Solarium\Core\Client\Adapter\ConnectionTimeoutAwareInterface;
use Solarium\Core\Client\Adapter\Curl;
use Solarium\Core\Client\Adapter\TimeoutAwareInterface;
use Solarium\Core\Client\Request;
use Solarium\Core\Client\Response;
use Solarium\Core\Event\Events;
use Solarium\Core\Event\PreExecuteRequest;
use Solarium\Core\Plugin\AbstractPlugin;
use Solarium\Exception\HttpException;

/**
* NoWaitForResponseRequest plugin.
*
* Long-running requests like suggest.buildAll might exceed timeouts.
* This plugin "tries" to convert the request in a kind of fire-and-forget.
* Most reliable if using the Curl adapter.
*/
class NoWaitForResponseRequest extends AbstractPlugin
{
/**
* Event hook to adjust client settings just before query execution.
*
* @param object $event
*
* @return self Provides fluent interface
*
* @throws \Solarium\Exception\HttpException
*/
public function preExecuteRequest($event): self
{
// We need to accept event proxies or decorators.
/** @var PreExecuteRequest $event */
$request = $event->getRequest();
$queryString = $request->getQueryString();

if (Request::METHOD_GET === $request->getMethod()) {
// GET requests usually expect a result. Since the purpose of this
// plugin is to trigger a long-running command and to not wait for
// its result, POST is the correct method.
// Depending on the HTTP configuration, GET requests could be
// cached. If this plugin is used, someone usually wants to build a
// dictionary or suggester and caching has to be avoided. Even if
// Solr accepts GET requests for these tasks, POST is the correct
// method.
$charset = $request->getParam('ie') ?? 'utf-8';
$request->setMethod(Request::METHOD_POST);
$request->setContentType(Request::CONTENT_TYPE_APPLICATION_X_WWW_FORM_URLENCODED, ['charset' => $charset]);
$request->setRawData($queryString);
$request->clearParams();
}

$timeout = TimeoutAwareInterface::DEFAULT_TIMEOUT;
if ($this->client->getAdapter() instanceof TimeoutAwareInterface) {
$timeout = $this->client->getAdapter()->getTimeout();
if (($this->client->getAdapter() instanceof ConnectionTimeoutAwareInterface) && ($this->client->getAdapter()->getConnectionTimeout() > 0)) {
$this->client->getAdapter()->setTimeout($this->client->getAdapter()->getConnectionTimeout() + TimeoutAwareInterface::FAST_TIMEOUT);
} else {
$this->client->getAdapter()->setTimeout(TimeoutAwareInterface::FAST_TIMEOUT);
}
}

if ($this->client->getAdapter() instanceof Curl) {
$this->client->getAdapter()->setOption('return_transfer', false);
}

$exception = null;
$microtime1 = microtime(true);
try {
$this->client->getAdapter()->execute($request, $event->getEndpoint());
} catch (HttpException $e) {
// We expect to run into a timeout.
$microtime2 = microtime(true);

if (($this->client->getAdapter() instanceof Curl) && (CURLE_OPERATION_TIMEDOUT != $e->getCode())) {
// An unexpected exception occurred.
$exception = $e;
} else {
$time_passed = $microtime2 - $microtime1;
if (($this->client->getAdapter() instanceof ConnectionTimeoutAwareInterface) && ($time_passed > $this->client->getAdapter()->getConnectionTimeout()) && ($time_passed < ($this->client->getAdapter()->getConnectionTimeout() + TimeoutAwareInterface::FAST_TIMEOUT))) {
// A connection timeout occurred, so the POST request has not been sent.
$exception = $e;
}
}
} catch (\Exception $exception) {
// Throw this exception after resetting the adapter.
}

if ($this->client->getAdapter() instanceof TimeoutAwareInterface) {
// Restore the previous timeout.
$this->client->getAdapter()->setTimeout($timeout);
}

if ($this->client->getAdapter() instanceof Curl) {
$this->client->getAdapter()->setOption('return_transfer', true);
}

if ($exception) {
throw $exception;
}

$response = new Response('', ['HTTP/1.0 200 OK']);
$event->setResponse($response);

return $this;
}

/**
* Plugin init function.
*
* Register event listeners.
*/
protected function initPluginType()
{
$dispatcher = $this->client->getEventDispatcher();
if (is_subclass_of($dispatcher, '\Symfony\Component\EventDispatcher\EventDispatcherInterface')) {
// NoWaitForResponseRequest has to act on PRE_EXECUTE_REQUEST before Loadbalancer (priority 0)
// and after PostBigRequest (priority 10). Set priority to 5.
$dispatcher->addListener(Events::PRE_EXECUTE_REQUEST, [$this, 'preExecuteRequest'], 5);
}
}

/**
* Plugin cleanup function.
*
* Unregister event listeners.
*/
public function deinitPlugin()
{
$dispatcher = $this->client->getEventDispatcher();
if (is_subclass_of($dispatcher, '\Symfony\Component\EventDispatcher\EventDispatcherInterface')) {
$dispatcher->removeListener(Events::PRE_EXECUTE_REQUEST, [$this, 'preExecuteRequest']);
}
}
}
1 change: 1 addition & 0 deletions src/QueryType/Suggester/Query.php
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ class Query extends BaseQuery implements SuggesterInterface, QueryInterface
'omitheader' => true,
'build' => false,
'reload' => false,
'buildAll' => false,
];

/**
Expand Down
2 changes: 1 addition & 1 deletion tests/Core/Client/Adapter/HttpTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ public function testExecute()
$mock->expects($this->once())
->method('getData')
->with($this->equalTo('http://127.0.0.1:8983/solr/'), $this->isType('resource'))
->willReturn([$data, ['HTTP 1.1 200 OK']]);
->willReturn([$data, ['HTTP/1.1 200 OK']]);

$mock->execute($request, $endpoint);
}
Expand Down
Loading

0 comments on commit 913c00b

Please sign in to comment.