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     */
48    public $host;
49
50    /**
51     * @internal Public for testing only
52     */
53    public $port;
54
55    /**
56     * @param array $conf
57     */
58    public function __construct( $conf ) {
59        if ( !count( $conf['servers'] ) ) {
60            throw new InvalidArgumentException( __METHOD__ . ': no servers configured' );
61        }
62        $this->hostNames = $conf['servers'];
63        $this->timeout = $conf['timeout'] ?? 0.1;
64        $this->connect_timeout = $conf['connect_timeout'] ?? 0;
65    }
66
67    /**
68     * @param string $key
69     * @return Status
70     */
71    public function get( $key ) {
72        $hashes = [];
73        foreach ( $this->hostNames as $hostName ) {
74            $hashes[$hostName] = md5( $hostName . $key );
75        }
76        asort( $hashes );
77        $errno = 0;
78        $errstr = '';
79        $hostName = '';
80        $conn = null;
81        foreach ( $hashes as $hostName => $hash ) {
82            if ( isset( $this->conns[$hostName] ) ) {
83                $this->refCounts[$hostName]++;
84                return Status::newGood(
85                    [ 'conn' => $this->conns[$hostName], 'hostName' => $hostName ] );
86            }
87            $parts = IPUtils::splitHostAndPort( $hostName );
88            if ( $parts === false ) {
89                $errstr = '\'servers\' config incorrectly configured.';
90                return Status::newFatal( 'poolcounter-connection-error', $errstr, $hostName );
91            }
92            // IPV6 addresses need to be in brackets otherwise it fails.
93            $this->host = IPUtils::isValidIPv6( $parts[0] ) ? '[' . $parts[0] . ']' : $parts[0];
94            $this->port = $parts[1] ?: 7531;
95            // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
96            $conn = @$this->open( $this->host, $this->port, $errno, $errstr );
97            if ( $conn ) {
98                break;
99            }
100        }
101        if ( !$conn ) {
102            return Status::newFatal( 'poolcounter-connection-error', $errstr, $hostName );
103        }
104        // TODO: Inject PSR Logger from ServiceWiring
105        wfDebug( "Connected to pool counter server: $hostName\n" );
106        $this->conns[$hostName] = $conn;
107        $this->refCounts[$hostName] = 1;
108        return Status::newGood( [ 'conn' => $conn, 'hostName' => $hostName ] );
109    }
110
111    /**
112     * Open a socket. Just a wrapper for fsockopen()
113     * @param string $host
114     * @param int $port
115     * @param int &$errno
116     * @param string &$errstr
117     * @return null|resource
118     */
119    private function open( $host, $port, &$errno, &$errstr ) {
120        // If connect_timeout is set, we try to open the socket twice.
121        // You usually want to set the connection timeout to a very
122        // small value so that in case of failure of a server the
123        // connection to poolcounter is not a SPOF.
124        if ( $this->connect_timeout > 0 ) {
125            $tries = 2;
126            $timeout = $this->connect_timeout;
127        } else {
128            $tries = 1;
129            $timeout = $this->timeout;
130        }
131
132        $fp = null;
133        while ( true ) {
134            $fp = fsockopen( $host, $port, $errno, $errstr, $timeout );
135            if ( $fp !== false || --$tries < 1 ) {
136                break;
137            }
138            usleep( 1000 );
139        }
140
141        return $fp;
142    }
143
144    /**
145     * @param resource $conn
146     */
147    public function close( $conn ) {
148        foreach ( $this->conns as $hostName => $otherConn ) {
149            if ( $conn === $otherConn ) {
150                if ( $this->refCounts[$hostName] ) {
151                    $this->refCounts[$hostName]--;
152                }
153                if ( !$this->refCounts[$hostName] ) {
154                    fclose( $conn );
155                    unset( $this->conns[$hostName] );
156                }
157            }
158        }
159    }
160}