Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 69
0.00% covered (danger)
0.00%
0 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
ElasticaConnection
0.00% covered (danger)
0.00%
0 / 68
0.00% covered (danger)
0.00%
0 / 7
552
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
0.00% covered (danger)
0.00%
0 / 51
0.00% covered (danger)
0.00%
0 / 1
182
 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\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 */
28abstract 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
205class_alias( ElasticaConnection::class, 'ElasticaConnection' );