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