Skip to content

Commit c62e157

Browse files
committed
Added http_client_service and http_client_options connection options.
[BC] [bugfix] Removed the request_timeout connection setting introduced in the last minor release as it might break depending on the http client used
1 parent 83e7255 commit c62e157

File tree

8 files changed

+529
-19
lines changed

8 files changed

+529
-19
lines changed

config/services.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ services:
2323
- # connection name
2424
- # connection settings
2525
- '%kernel.debug%'
26+
- # optional PSR-18 HTTP client service
2627

2728
sfes.index_manager_prototype:
2829
abstract: true

docs/configuration.md

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,10 @@ sineflow_elasticsearch:
2626
profiling_backtrace: false
2727
logging: true
2828
bulk_batch_size: 1000
29-
request_timeout: 300
29+
http_client_service: 'app.elasticsearch.http_client' # Optional: Service ID of custom PSR-18 HTTP client
30+
http_client_options: # If using the recommended Guzzle HTTP client
31+
timeout: 0 # No timeout (the default behaviour of Guzzle)
32+
connect_timeout: 5 # Try connecting for max 5 seconds (so we don't block in case of an unresponsive node)
3033
3134
indices:
3235
_base:
@@ -82,7 +85,8 @@ And here is the breakdown:
8285
* `profiling` *(default: true)*: Enable or disable profiling. The default setup makes use of Elasticsearch client's profiling to gather information for the Symfony profiler toolbar, which is extremely useful in development.
8386
* `logging` *(default: true)*: When enabled, the bundle uses Symfony's 'logger' service to log Elasticsearch events in the 'sfes' channel. Using symfony/monolog-bundle, the logging can be easily controlled. For example the 'sfes' channel can be redirected to a rotating file log.
8487
* `bulk_batch_size` *(default: 1000)*: This is currently used only when using the **rebuildIndex()** method of the index manager.
85-
* `request_timeout` *(default: 300)*: The timeout for HTTP requests to Elasticsearch in seconds. This applies to all synchronous operations including reindex, bulk operations, and long-running queries. Increase this value if you have long-running operations that exceed the default timeout.
88+
* `http_client_service` *(default: null)*: Service ID of a PSR-18 HTTP client to use for this connection (e.g., `'app.elasticsearch.http_client'`). When specified, this custom HTTP client will be injected into the Elasticsearch client instead of using auto-discovery. If not specified, the Elasticsearch client will auto-discover an available HTTP client implementation. **Note:** Auto-discovery is not recommended for production environments - always specify an explicit HTTP client service for deterministic behavior and to avoid discovery overhead.
89+
* `http_client_options` *(default: [])*: HTTP client-specific parameters passed directly to the underlying PSR-18 HTTP client (Guzzle, Symfony HttpClient, etc.). This allows you to configure timeout and connection behavior for any HTTP client:
8690

8791
* `indices`: Here you define the Elasticsearch indexes you have. The key here is the name of the index manager, which determines how it will be accessible in the application. In the example above, we have an index manager named **products**, which would be accessible as **$container->get('sfes.index.products')**.
8892
It is important to note here the use of **'_'** in front of the index manager name. When defined like that, this will be an abstract definition, i.e. no manager will actually be created from that definition. This is very useful when you have common setting for several indices, as you can define a template for them all and not have to duplicate stuff.
@@ -92,6 +96,34 @@ It is important to note here the use of **'_'** in front of the index manager na
9296
* `settings`: Here you can specify any index settings supported by Elasticsearch. [See here for more info on that](https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-update-settings.html)
9397
* `types`: This is where you specify the types, which will be managed by the index. This is done by listing the document entities that manage the respective types, in short notation.
9498

99+
## Configuring custom HTTP client:
100+
101+
It is recommended to explicitly configure an HTTP client service for your Elasticsearch connections rather than relying on auto-discovery.
102+
103+
**For example:**
104+
105+
Use Guzzle HTTP Client (requires `guzzlehttp/guzzle` ^7.0):
106+
107+
```yaml
108+
# config/services.yaml
109+
services:
110+
app.guzzle.client:
111+
class: GuzzleHttp\Client
112+
arguments:
113+
- { http_errors: false } # Ensures PSR-18 compliance
114+
115+
# config/packages/elasticsearch.yaml
116+
sineflow_elasticsearch:
117+
connections:
118+
default:
119+
hosts:
120+
- 'elasticsearch.example.com:9200'
121+
http_client_service: 'app.guzzle.client'
122+
http_client_options:
123+
timeout: 0 # No timeout (the default behaviour of Guzzle)
124+
connect_timeout: 5 # Try connecting for max 5 seconds (so we don't block in case of an unresponsive node)
125+
```
126+
95127
## Configuring custom cache pool:
96128
97129
### Example (cache metadata in Redis):

src/DependencyInjection/Compiler/AddConnectionsPass.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,11 @@ public function process(ContainerBuilder $container): void
3030
$connectionDefinition->replaceArgument(0, $connectionName);
3131
$connectionDefinition->replaceArgument(1, $connectionSettings);
3232

33+
// Inject HTTP client service if specified
34+
if (!empty($connectionSettings['http_client_service'])) {
35+
$connectionDefinition->replaceArgument(3, new Reference($connectionSettings['http_client_service']));
36+
}
37+
3338
$connectionDefinition->addMethodCall('setEventDispatcher', [new Reference('event_dispatcher')]);
3439
if ($connectionSettings['logging']) {
3540
$connectionDefinition->addMethodCall('setLogger', [new Reference('logger')]);

src/DependencyInjection/Configuration.php

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -137,9 +137,16 @@ private function getConnectionsNode(): NodeDefinition
137137
->defaultValue(1000)
138138
->info('The number of requests to send at once, when doing bulk operations')
139139
->end()
140-
->scalarNode('request_timeout')
141-
->defaultValue(300)
142-
->info('Timeout for HTTP requests to Elasticsearch in seconds (default: 300)')
140+
->scalarNode('http_client_service')
141+
->info('Service ID of a PSR-18 HTTP client to use. If not specified, auto-discovery is used (not recommended for production).')
142+
->defaultNull()
143+
->end()
144+
->arrayNode('http_client_options')
145+
->info('HTTP client-specific options (passed directly to the underlying PSR-18 HTTP client).')
146+
->useAttributeAsKey('name')
147+
->normalizeKeys(false)
148+
->prototype('variable')->end()
149+
->defaultValue([])
143150
->end()
144151

145152
->end()

src/Manager/ConnectionManager.php

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use Elastic\Elasticsearch\Exception\InvalidArgumentException;
88
use Elastic\Elasticsearch\Exception\ServerResponseException;
99
use Elastic\Transport\Exception\NoNodeAvailableException;
10+
use Psr\Http\Client\ClientInterface;
1011
use Psr\Log\LoggerInterface;
1112
use Sineflow\ElasticsearchBundle\Client\Client;
1213
use Sineflow\ElasticsearchBundle\DTO\BulkQueryItem;
@@ -42,14 +43,16 @@ class ConnectionManager
4243
protected ?ProfilerQueryLogCollection $profilerQueryLogCollection = null;
4344

4445
/**
45-
* @param string $connectionName The unique connection name
46-
* @param array $connectionSettings Settings array
47-
* @param bool $kernelDebug The kernel.debug value
46+
* @param string $connectionName The unique connection name
47+
* @param array $connectionSettings Settings array
48+
* @param bool $kernelDebug The kernel.debug value
49+
* @param ClientInterface|null $httpClient Optional PSR-18 HTTP client to use
4850
*/
4951
public function __construct(
5052
protected string $connectionName,
5153
protected array $connectionSettings,
5254
protected bool $kernelDebug,
55+
protected ?ClientInterface $httpClient = null,
5356
) {
5457
$this->bulkQueries = [];
5558
$this->bulkParams = [];
@@ -83,6 +86,17 @@ public function getClient(): Client
8386
{
8487
if (!$this->client) {
8588
$clientBuilder = ClientBuilder::create();
89+
90+
// Use injected HTTP client if available
91+
if (null !== $this->httpClient) {
92+
$clientBuilder->setHttpClient($this->httpClient);
93+
} else {
94+
// Warn when using auto-discovery
95+
$this->logger?->notice('Using auto-discovered HTTP client. Consider configuring http_client_service for deterministic behavior.', [
96+
'connection' => $this->connectionName,
97+
]);
98+
}
99+
86100
$clientBuilder->setHosts($this->connectionSettings['hosts']);
87101

88102
// Configure basic auth
@@ -107,13 +121,9 @@ public function getClient(): Client
107121
$clientBuilder->setSSLCert($this->connectionSettings['ssl_cert']);
108122
}
109123

110-
// Configure HTTP client timeout
111-
$httpClientOptions = [];
112-
if (isset($this->connectionSettings['request_timeout'])) {
113-
$httpClientOptions['timeout'] = $this->connectionSettings['request_timeout'];
114-
}
115-
if (!empty($httpClientOptions)) {
116-
$clientBuilder->setHttpClientOptions($httpClientOptions);
124+
// Check if there are any HTTP client options specified and set them to the connection
125+
if (!empty($this->connectionSettings['http_client_options'] ?? [])) {
126+
$clientBuilder->setHttpClientOptions($this->connectionSettings['http_client_options']);
117127
}
118128

119129
if ($this->logger) {
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
<?php
2+
3+
namespace Sineflow\ElasticsearchBundle\Tests\Functional\Manager;
4+
5+
use Psr\Http\Client\ClientInterface;
6+
use Sineflow\ElasticsearchBundle\Manager\ConnectionManager;
7+
use Sineflow\ElasticsearchBundle\Tests\AbstractContainerAwareTestCase;
8+
9+
class ConnectionManagerTest extends AbstractContainerAwareTestCase
10+
{
11+
public function testConnectionManagerWithoutHttpClientService(): void
12+
{
13+
$connectionManager = $this->getContainer()->get('sfes.connection.default');
14+
15+
$this->assertInstanceOf(ConnectionManager::class, $connectionManager);
16+
17+
// Use reflection to verify HTTP client is null (will use auto-discovery)
18+
$reflection = new \ReflectionClass($connectionManager);
19+
$httpClientProperty = $reflection->getProperty('httpClient');
20+
$httpClientProperty->setAccessible(true);
21+
22+
$this->assertNull($httpClientProperty->getValue($connectionManager), 'Default connection should not have a custom HTTP client injected');
23+
}
24+
25+
public function testConnectionManagerHasHttpClientOptions(): void
26+
{
27+
$connectionManager = $this->getContainer()->get('sfes.connection.default');
28+
29+
$this->assertInstanceOf(ConnectionManager::class, $connectionManager);
30+
31+
// Use reflection to access connection settings
32+
$reflection = new \ReflectionClass($connectionManager);
33+
$settingsProperty = $reflection->getProperty('connectionSettings');
34+
$settingsProperty->setAccessible(true);
35+
36+
$settings = $settingsProperty->getValue($connectionManager);
37+
38+
$this->assertIsArray($settings);
39+
$this->assertArrayHasKey('http_client_options', $settings, 'Connection settings should contain http_client_options key');
40+
$this->assertIsArray($settings['http_client_options'], 'http_client_options should be an array');
41+
}
42+
43+
public function testConnectionManagerWithCustomHttpClientService(): void
44+
{
45+
// Create a mock HTTP client
46+
$mockHttpClient = $this->createMock(ClientInterface::class);
47+
48+
// Register it as a service
49+
$container = $this->getContainer();
50+
$container->set('app.test_http_client', $mockHttpClient);
51+
52+
// Create a ConnectionManager with the custom HTTP client
53+
$connectionManager = new ConnectionManager(
54+
'test_custom_client',
55+
[
56+
'hosts' => ['localhost:9200'],
57+
'profiling' => false,
58+
'logging' => false,
59+
'bulk_batch_size' => 100,
60+
'http_client_options' => [
61+
'timeout' => 45,
62+
'max_duration' => 600,
63+
],
64+
'http_client_service' => 'app.test_http_client',
65+
],
66+
true, // debug mode
67+
$mockHttpClient
68+
);
69+
70+
// Use reflection to verify the HTTP client is set
71+
$reflection = new \ReflectionClass($connectionManager);
72+
$httpClientProperty = $reflection->getProperty('httpClient');
73+
$httpClientProperty->setAccessible(true);
74+
75+
$injectedClient = $httpClientProperty->getValue($connectionManager);
76+
$this->assertSame($mockHttpClient, $injectedClient, 'Custom HTTP client should be injected into ConnectionManager');
77+
}
78+
79+
public function testConnectionManagerPreservesHttpClientOptions(): void
80+
{
81+
$customOptions = [
82+
'timeout' => 30,
83+
'connect_timeout' => 10,
84+
'max_duration' => 3600,
85+
'extra' => [
86+
'curl' => [
87+
CURLOPT_SSL_VERIFYPEER => false,
88+
],
89+
],
90+
];
91+
92+
$connectionManager = new ConnectionManager(
93+
'test_options',
94+
[
95+
'hosts' => ['localhost:9200'],
96+
'profiling' => false,
97+
'logging' => false,
98+
'bulk_batch_size' => 100,
99+
'http_client_options' => $customOptions,
100+
'http_client_service' => null,
101+
],
102+
true,
103+
null
104+
);
105+
106+
// Use reflection to access connection settings
107+
$reflection = new \ReflectionClass($connectionManager);
108+
$settingsProperty = $reflection->getProperty('connectionSettings');
109+
$settingsProperty->setAccessible(true);
110+
111+
$settings = $settingsProperty->getValue($connectionManager);
112+
113+
$this->assertArrayHasKey('http_client_options', $settings);
114+
$this->assertSame($customOptions, $settings['http_client_options'], 'HTTP client options should be preserved exactly as configured');
115+
116+
// Verify nested options are preserved
117+
$this->assertIsArray($settings['http_client_options']['extra']);
118+
$this->assertIsArray($settings['http_client_options']['extra']['curl']);
119+
$this->assertFalse($settings['http_client_options']['extra']['curl'][CURLOPT_SSL_VERIFYPEER]);
120+
}
121+
122+
public function testConnectionManagerWithBothHttpClientServiceAndOptions(): void
123+
{
124+
$mockHttpClient = $this->createMock(ClientInterface::class);
125+
126+
$customOptions = [
127+
'timeout' => 60,
128+
'max_duration' => 120,
129+
];
130+
131+
$connectionManager = new ConnectionManager(
132+
'test_both',
133+
[
134+
'hosts' => ['localhost:9200'],
135+
'profiling' => false,
136+
'logging' => false,
137+
'bulk_batch_size' => 100,
138+
'http_client_options' => $customOptions,
139+
'http_client_service' => 'app.test_http_client',
140+
],
141+
true,
142+
$mockHttpClient
143+
);
144+
145+
// Verify HTTP client is injected
146+
$reflection = new \ReflectionClass($connectionManager);
147+
$httpClientProperty = $reflection->getProperty('httpClient');
148+
$httpClientProperty->setAccessible(true);
149+
150+
$injectedClient = $httpClientProperty->getValue($connectionManager);
151+
$this->assertSame($mockHttpClient, $injectedClient);
152+
153+
// Verify HTTP client options are preserved
154+
$settingsProperty = $reflection->getProperty('connectionSettings');
155+
$settingsProperty->setAccessible(true);
156+
157+
$settings = $settingsProperty->getValue($connectionManager);
158+
$this->assertSame($customOptions, $settings['http_client_options']);
159+
}
160+
}

0 commit comments

Comments
 (0)