Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
21.13% covered (danger)
21.13%
15 / 71
0.00% covered (danger)
0.00%
0 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
ElasticaConnection
21.43% covered (danger)
21.43%
15 / 70
0.00% covered (danger)
0.00%
0 / 7
279.60
0.00% covered (danger)
0.00%
0 / 1
 getServerList
n/a
0 / 0
n/a
0 / 0
0
 getMaxConnectionAttempts
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setTimeout
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 setConnectTimeout
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 getClient
28.30% covered (danger)
28.30%
15 / 53
0.00% covered (danger)
0.00%
0 / 1
75.29
 getIndex
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getIndexName
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
 destroyClient
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3namespace MediaWiki\Extension\Elastica;
4
5use Elastica\Client;
6use Elastica\Index;
7use Mediawiki\Http\Telemetry;
8use 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 */
30abstract 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
208class_alias( ElasticaConnection::class, 'ElasticaConnection' );