Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
21.13% |
15 / 71 |
|
0.00% |
0 / 7 |
CRAP | |
0.00% |
0 / 1 |
ElasticaConnection | |
21.43% |
15 / 70 |
|
0.00% |
0 / 7 |
279.60 | |
0.00% |
0 / 1 |
getServerList | n/a |
0 / 0 |
n/a |
0 / 0 |
0 | |||||
getMaxConnectionAttempts | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
setTimeout | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
setConnectTimeout | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
getClient | |
28.30% |
15 / 53 |
|
0.00% |
0 / 1 |
75.29 | |||
getIndex | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getIndexName | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
12 | |||
destroyClient | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 |
1 | <?php |
2 | |
3 | namespace MediaWiki\Extension\Elastica; |
4 | |
5 | use Elastica\Client; |
6 | use Elastica\Index; |
7 | use Mediawiki\Http\Telemetry; |
8 | use MediaWiki\Logger\LoggerFactory; |
9 | |
10 | /** |
11 | * Forms and caches connection to Elasticsearch as well as client objects |
12 | * that contain connection information like \Elastica\Index. Propagates |
13 | * distributed tracing headers. |
14 | * |
15 | * This program is free software; you can redistribute it and/or modify |
16 | * it under the terms of the GNU General Public License as published by |
17 | * the Free Software Foundation; either version 2 of the License, or |
18 | * (at your option) any later version. |
19 | * |
20 | * This program is distributed in the hope that it will be useful, |
21 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
22 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
23 | * GNU General Public License for more details. |
24 | * |
25 | * You should have received a copy of the GNU General Public License along |
26 | * with this program; if not, write to the Free Software Foundation, Inc., |
27 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
28 | * http://www.gnu.org/copyleft/gpl.html |
29 | */ |
30 | abstract class ElasticaConnection { |
31 | /** |
32 | * @var ?Client |
33 | */ |
34 | protected $client; |
35 | |
36 | /** |
37 | * @return string[] server ips or hostnames |
38 | */ |
39 | abstract public function getServerList(); |
40 | |
41 | /** |
42 | * How many times can we attempt to connect per host? |
43 | * |
44 | * @return int |
45 | */ |
46 | public function getMaxConnectionAttempts() { |
47 | return 1; |
48 | } |
49 | |
50 | /** |
51 | * Set the client side timeout to be used for the rest of this process. |
52 | * @param int $timeout timeout in seconds |
53 | */ |
54 | public function setTimeout( $timeout ) { |
55 | $client = $this->getClient(); |
56 | // Set the timeout for new connections |
57 | $client->setConfigValue( 'timeout', $timeout ); |
58 | foreach ( $client->getConnections() as $connection ) { |
59 | $connection->setTimeout( $timeout ); |
60 | } |
61 | } |
62 | |
63 | /** |
64 | * Set the client side connect timeout to be used for the rest of this process. |
65 | * @param int $timeout timeout in seconds |
66 | */ |
67 | public function setConnectTimeout( $timeout ) { |
68 | $client = $this->getClient(); |
69 | // Set the timeout for new connections |
70 | $client->setConfigValue( 'connectTimeout', $timeout ); |
71 | foreach ( $client->getConnections() as $connection ) { |
72 | $connection->setConnectTimeout( $timeout ); |
73 | } |
74 | } |
75 | |
76 | /** |
77 | * Fetch a connection. |
78 | * @return Client |
79 | */ |
80 | public function getClient() { |
81 | if ( $this->client === null ) { |
82 | // Setup the Elastica servers |
83 | $servers = []; |
84 | $serverList = $this->getServerList(); |
85 | if ( !is_array( $serverList ) ) { |
86 | $serverList = [ $serverList ]; |
87 | } |
88 | foreach ( $serverList as $server ) { |
89 | if ( !is_array( $server ) ) { |
90 | $server = [ 'host' => $server ]; |
91 | } |
92 | $server['headers'] = ( $server['headers'] ?? [] ) |
93 | + Telemetry::getInstance()->getRequestHeaders(); |
94 | $servers[] = $server; |
95 | } |
96 | |
97 | $this->client = new Client( [ 'servers' => $servers ], |
98 | /** |
99 | * Callback for \Elastica\Client on request failures. |
100 | * @param \Elastica\Connection $connection The current connection to elasticasearch |
101 | * @param \Exception $e Exception to be thrown if we don't do anything |
102 | * @param \Elastica\Client $client |
103 | */ |
104 | function ( $connection, $e, $client ) { |
105 | // We only want to try to reconnect on http connection errors |
106 | // Beyond that we want to give up fast. Configuring a single connection |
107 | // through LVS accomplishes this. |
108 | if ( !( $e instanceof \Elastica\Exception\Connection\HttpException ) ) { |
109 | LoggerFactory::getInstance( 'Elastica' ) |
110 | ->error( 'Unknown connection exception communicating with Elasticsearch: {class_name}', |
111 | [ 'class_name' => get_class( $e ) ] ); |
112 | return; |
113 | } |
114 | if ( $e->getError() === CURLE_OPERATION_TIMEOUTED ) { |
115 | // Timeouts shouldn't disable the connection and should always be thrown |
116 | // back to the caller so they can catch it and handle it. They should |
117 | // never be retried blindly. |
118 | $connection->setEnabled( true ); |
119 | throw $e; |
120 | } |
121 | if ( $e->getError() === CURLE_PARTIAL_FILE ) { |
122 | // This means the connection dropped before the full response was read, |
123 | // likely some sort of network problem or elasticsearch shut down |
124 | // mid-response. If the network failed or elasticsearch is gone the |
125 | // retry should fail, but we delegate deciding on retries to the caller. |
126 | LoggerFactory::getInstance( 'Elastica' ) |
127 | ->error( 'Error communicating with elasticsearch, connection closed' . |
128 | 'before full response was read.', [ 'exception' => $e ] ); |
129 | $connection->setEnabled( true ); |
130 | throw $e; |
131 | } |
132 | if ( $e->getError() !== CURLE_COULDNT_CONNECT ) { |
133 | LoggerFactory::getInstance( 'Elastica' ) |
134 | ->error( 'Unexpected connection error communicating with Elasticsearch. ' . |
135 | 'Curl code: {curl_code}', [ 'curl_code' => $e->getError() ] ); |
136 | // If there are different connections we could try leave this connection disabled |
137 | // and let Elastica retry on a different connection. |
138 | if ( $client->hasConnection() ) { |
139 | return; |
140 | } |
141 | // Otherwise this was the last available connection. Re-enable it but throw |
142 | // so that retries are delegated to the application. This prevents the |
143 | // situation where the calling code knows it can retry but no connections remain. |
144 | $connection->setEnabled( true ); |
145 | throw $e; |
146 | } |
147 | // Keep track of the number of times we've hit a host |
148 | static $connectionAttempts = []; |
149 | $host = $connection->getParam( 'host' ); |
150 | $connectionAttempts[ $host ] = isset( $connectionAttempts[ $host ] ) |
151 | ? $connectionAttempts[ $host ] + 1 : 1; |
152 | |
153 | // Check if we've hit the host the max # of times. If not, try again |
154 | if ( $connectionAttempts[ $host ] < $this->getMaxConnectionAttempts() ) { |
155 | LoggerFactory::getInstance( 'Elastica' ) |
156 | ->info( "Retrying connection to {elastic_host} after {attempts} attempts", |
157 | [ |
158 | 'elastic_host' => $host, |
159 | 'attempts' => $connectionAttempts[ $host ], |
160 | ] ); |
161 | $connection->setEnabled( true ); |
162 | } elseif ( !$client->hasConnection() ) { |
163 | // Don't disable the last connection, but don't let it auto-retry either. |
164 | $connection->setEnabled( true ); |
165 | throw $e; |
166 | } |
167 | } |
168 | ); |
169 | } |
170 | |
171 | return $this->client; |
172 | } |
173 | |
174 | /** |
175 | * Fetch the Elastica Index. |
176 | * @param string $name get the index(es) with this basename |
177 | * @param string|bool $type type of index (named type or false to get all) |
178 | * @param mixed $identifier if specified get the named identifier of the index |
179 | * @return Index |
180 | */ |
181 | public function getIndex( $name, $type = false, $identifier = false ) { |
182 | return $this->getClient()->getIndex( $this->getIndexName( $name, $type, $identifier ) ); |
183 | } |
184 | |
185 | /** |
186 | * Get the name of the index. |
187 | * @param string $name get the index(es) with this basename |
188 | * @param string|bool $type type of index (named type or false to get all) |
189 | * @param mixed $identifier if specified get the named identifier of the index |
190 | * @return string name of index for $type and $identifier |
191 | */ |
192 | public function getIndexName( $name, $type = false, $identifier = false ) { |
193 | if ( $type ) { |
194 | $name .= '_' . $type; |
195 | } |
196 | if ( $identifier ) { |
197 | $name .= '_' . $identifier; |
198 | } |
199 | return $name; |
200 | } |
201 | |
202 | public function destroyClient() { |
203 | $this->client = null; |
204 | ElasticaHttpTransportCloser::destroySingleton(); |
205 | } |
206 | } |
207 | |
208 | class_alias( ElasticaConnection::class, 'ElasticaConnection' ); |