Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
59.62% covered (warning)
59.62%
31 / 52
0.00% covered (danger)
0.00%
0 / 4
CRAP
0.00% covered (danger)
0.00%
0 / 1
PoolCounterConnectionManager
59.62% covered (warning)
59.62%
31 / 52
0.00% covered (danger)
0.00%
0 / 4
50.05
0.00% covered (danger)
0.00%
0 / 1
 __construct
80.00% covered (warning)
80.00%
4 / 5
0.00% covered (danger)
0.00%
0 / 1
2.03
 get
64.29% covered (warning)
64.29%
18 / 28
0.00% covered (danger)
0.00%
0 / 1
12.69
 open
75.00% covered (warning)
75.00%
9 / 12
0.00% covered (danger)
0.00%
0 / 1
5.39
 close
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
30
1<?php
2/**
3 * This program is free software; you can redistribute it and/or modify
4 * it under the terms of the GNU General Public License as published by
5 * the Free Software Foundation; either version 2 of the License, or
6 * (at your option) any later version.
7 *
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
12 *
13 * You should have received a copy of the GNU General Public License along
14 * with this program; if not, write to the Free Software Foundation, Inc.,
15 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 * http://www.gnu.org/copyleft/gpl.html
17 *
18 * @file
19 */
20
21namespace MediaWiki\PoolCounter;
22
23use InvalidArgumentException;
24use MediaWiki\Status\Status;
25use Wikimedia\IPUtils;
26
27/**
28 * Helper for \MediaWiki\PoolCounter\PoolCounterClient.
29 *
30 * @internal
31 * @since 1.16
32 */
33class PoolCounterConnectionManager {
34    /** @var string[] */
35    public $hostNames;
36    /** @var array */
37    public $conns = [];
38    /** @var array */
39    public $refCounts = [];
40    /** @var float */
41    public $timeout;
42    /** @var int */
43    public $connect_timeout;
44
45    /**
46     * @internal Public for testing only
47     * @var string
48     */
49    public $host;
50
51    /**
52     * @internal Public for testing only
53     * @var int
54     */
55    public $port;
56
57    /**
58     * @param array $conf
59     */
60    public function __construct( $conf ) {
61        if ( !count( $conf['servers'] ) ) {
62            throw new InvalidArgumentException( __METHOD__ . ': no servers configured' );
63        }
64        $this->hostNames = $conf['servers'];
65        $this->timeout = $conf['timeout'] ?? 0.1;
66        $this->connect_timeout = $conf['connect_timeout'] ?? 0;
67    }
68
69    /**
70     * @param string $key
71     * @return Status
72     */
73    public function get( $key ) {
74        $hashes = [];
75        foreach ( $this->hostNames as $hostName ) {
76            $hashes[$hostName] = md5( $hostName . $key );
77        }
78        asort( $hashes );
79        $errno = 0;
80        $errstr = '';
81        $hostName = '';
82        $conn = null;
83        foreach ( $hashes as $hostName => $hash ) {
84            if ( isset( $this->conns[$hostName] ) ) {
85                $this->refCounts[$hostName]++;
86                return Status::newGood(
87                    [ 'conn' => $this->conns[$hostName], 'hostName' => $hostName ] );
88            }
89            $parts = IPUtils::splitHostAndPort( $hostName );
90            if ( $parts === false ) {
91                $errstr = '\'servers\' config incorrectly configured.';
92                return Status::newFatal( 'poolcounter-connection-error', $errstr, $hostName );
93            }
94            // IPV6 addresses need to be in brackets otherwise it fails.
95            $this->host = IPUtils::isValidIPv6( $parts[0] ) ? '[' . $parts[0] . ']' : $parts[0];
96            $this->port = $parts[1] ?: 7531;
97            // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
98            $conn = @$this->open( $this->host, $this->port, $errno, $errstr );
99            if ( $conn ) {
100                break;
101            }
102        }
103        if ( !$conn ) {
104            return Status::newFatal( 'poolcounter-connection-error', $errstr, $hostName );
105        }
106        // TODO: Inject PSR Logger from ServiceWiring
107        wfDebug( "Connected to pool counter server: $hostName\n" );
108        $this->conns[$hostName] = $conn;
109        $this->refCounts[$hostName] = 1;
110        return Status::newGood( [ 'conn' => $conn, 'hostName' => $hostName ] );
111    }
112
113    /**
114     * Open a socket. Just a wrapper for fsockopen()
115     * @param string $host
116     * @param int $port
117     * @param int &$errno
118     * @param string &$errstr
119     * @return null|resource
120     */
121    private function open( $host, $port, &$errno, &$errstr ) {
122        // If connect_timeout is set, we try to open the socket twice.
123        // You usually want to set the connection timeout to a very
124        // small value so that in case of failure of a server the
125        // connection to poolcounter is not a SPOF.
126        if ( $this->connect_timeout > 0 ) {
127            $tries = 2;
128            $timeout = $this->connect_timeout;
129        } else {
130            $tries = 1;
131            $timeout = $this->timeout;
132        }
133
134        $fp = null;
135        while ( true ) {
136            $fp = fsockopen( $host, $port, $errno, $errstr, $timeout );
137            if ( $fp !== false || --$tries < 1 ) {
138                break;
139            }
140            usleep( 1000 );
141        }
142
143        return $fp;
144    }
145
146    /**
147     * @param resource $conn
148     */
149    public function close( $conn ) {
150        foreach ( $this->conns as $hostName => $otherConn ) {
151            if ( $conn === $otherConn ) {
152                if ( $this->refCounts[$hostName] ) {
153                    $this->refCounts[$hostName]--;
154                }
155                if ( !$this->refCounts[$hostName] ) {
156                    fclose( $conn );
157                    unset( $this->conns[$hostName] );
158                }
159            }
160        }
161    }
162}