Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
67.44% covered (warning)
67.44%
29 / 43
57.14% covered (warning)
57.14%
4 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
HaproxyStatusParser
67.44% covered (warning)
67.44%
29 / 43
57.14% covered (warning)
57.14%
4 / 7
22.77
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
19 / 19
100.00% covered (success)
100.00%
1 / 1
6
 findServerRowIndex
80.00% covered (warning)
80.00%
4 / 5
0.00% covered (danger)
0.00%
0 / 1
4.13
 findServerColumnValue
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 getColumnValue
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getNumberOfRows
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isQueueOverloaded
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
 getAvailableNonQueuedConnectionSlots
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3namespace MediaWiki\Wikispeech;
4
5/**
6 * @file
7 * @ingroup Extensions
8 * @license GPL-2.0-or-later
9 */
10
11use MWException;
12
13/**
14 * Parses output from HAProxy stats CSV.
15 *
16 * Known columns:
17 *
18 * 0. pxname [LFBS]: proxy name
19 * 1. svname [LFBS]: service name
20 *  (FRONTEND for frontend, BACKEND for backend, any name for server/listener)
21 * 2. qcur [..BS]: current queued requests.
22 *  For the backend this reports the number queued without a server assigned.
23 * 3. qmax [..BS]: max value of qcur
24 * 4. scur [LFBS]: current sessions
25 * 5. smax [LFBS]: max sessions
26 * 6. slim [LFBS]: configured session limit
27 * 7. stot [LFBS]: cumulative number of connections
28 * 8. bin [LFBS]: bytes in
29 * 9. bout [LFBS]: bytes out
30 * 10. dreq [LFB.]: requests denied because of security concerns.
31 *  - For tcp this is because of a matched tcp-request content rule.
32 *  - For http this is because of a matched http-request or tarpit rule.
33 * 11. dresp [LFBS]: responses denied because of security concerns.
34 *  - For http this is because of a matched http-request rule, or "option checkcache".
35 * 12. ereq [LF..]: request errors. Some of the possible causes are:
36 *  - early termination from the client, before the request has been sent.
37 *  - read error from the client
38 *  - client timeout
39 *  - client closed connection
40 *  - various bad requests from the client.
41 *  - request was tarpitted.
42 * 13. econ [..BS]: number of requests that encountered an error trying to connect to
43 *  a backend server. The backend stat is the sum of the stat for all servers of that backend,
44 *  plus any connection errors not associated with a particular server
45 *  (such as the backend having no active servers).
46 * 14. eresp [..BS]: response errors. srv_abrt will be counted here also.
47 *  Some other errors are:
48 *  - write error on the client socket (won't be counted for the server stat)
49 *  - failure applying filters to the response.
50 * 15. wretr [..BS]: number of times a connection to a server was retried.
51 * 16. wredis [..BS]: number of times a request was redispatched to another server.
52 *  The server value counts the number of times that server was switched away from.
53 * 17. status [LFBS]: status (UP/DOWN/NOLB/MAINT/MAINT(via)...)
54 * 18. weight [..BS]: total weight (backend), server weight (server)
55 * 19. act [..BS]: number of active servers (backend), server is active (server)
56 * 20. bck [..BS]: number of backup servers (backend), server is backup (server)
57 * 21. chkfail [...S]: number of failed checks. (Only counts checks failed when the server is up.)
58 * 22. chkdown [..BS]: number of UP->DOWN transitions. The backend counter counts transitions
59 *  to the whole backend being down, rather than the sum of the counters for each server.
60 * 23. lastchg [..BS]: number of seconds since the last UP<->DOWN transition
61 * 24. downtime [..BS]: total downtime (in seconds). The value for the backend
62 *  is the downtime for the whole backend, not the sum of the server downtime.
63 * 25. qlimit [...S]: configured maxqueue for the server,
64 *  or nothing in the value is 0 (default, meaning no limit)
65 * 26. pid [LFBS]: process id (0 for first instance, 1 for second, ...)
66 * 27. iid [LFBS]: unique proxy id
67 * 28. sid [L..S]: server id (unique inside a proxy)
68 * 29. throttle [...S]: current throttle percentage for the server, when slowstart is active,
69 *  or no value if not in slowstart.
70 * 30. lbtot [..BS]: total number of times a server was selected, either for new sessions,
71 *  or when re-dispatching. The server counter is the number of times that server was selected.
72 * 31. tracked [...S]: id of proxy/server if tracking is enabled.
73 * 32. type [LFBS]: (0=frontend, 1=backend, 2=server, 3=socket/listener)
74 * 33. rate [.FBS]: number of sessions per second over last elapsed second
75 * 34. rate_lim [.F..]: configured limit on new sessions per second
76 * 35. rate_max [.FBS]: max number of new sessions per second
77 * 36. check_status [...S]: status of last health check, one of:
78 *  UNK     -> unknown
79 *  INI     -> initializing
80 *  SOCKERR -> socket error
81 *  L4OK    -> check passed on layer 4, no upper layers testing enabled
82 *  L4TOUT  -> layer 1-4 timeout
83 *  L4CON   -> layer 1-4 connection problem, for example
84 *  "Connection refused" (tcp rst) or "No route to host" (icmp)
85 *  L6OK    -> check passed on layer 6
86 *  L6TOUT  -> layer 6 (SSL) timeout
87 *  L6RSP   -> layer 6 invalid response - protocol error
88 *  L7OK    -> check passed on layer 7
89 *  L7OKC   -> check conditionally passed on layer 7, for example 404 with
90 *  disable-on-404
91 *  L7TOUT  -> layer 7 (HTTP/SMTP) timeout
92 *  L7RSP   -> layer 7 invalid response - protocol error
93 *  L7STS   -> layer 7 response error, for example HTTP 5xx
94 * 37. check_code [...S]: layer5-7 code, if available
95 * 38. check_duration [...S]: time in ms took to finish last health check
96 * 39. hrsp_1xx [.FBS]: http responses with 1xx code
97 * 40. hrsp_2xx [.FBS]: http responses with 2xx code
98 * 41. hrsp_3xx [.FBS]: http responses with 3xx code
99 * 42. hrsp_4xx [.FBS]: http responses with 4xx code
100 * 43. hrsp_5xx [.FBS]: http responses with 5xx code
101 * 44. hrsp_other [.FBS]: http responses with other codes (protocol error)
102 * 45. hanafail [...S]: failed health checks details
103 * 46. req_rate [.F..]: HTTP requests per second over last elapsed second
104 * 47. req_rate_max [.F..]: max number of HTTP requests per second observed
105 * 48. req_tot [.F..]: total number of HTTP requests received
106 * 49. cli_abrt [..BS]: number of data transfers aborted by the client
107 * 50. srv_abrt [..BS]: number of data transfers aborted by the server (inc. in eresp)
108 * 51. comp_in [.FB.]: number of HTTP response bytes fed to the compressor
109 * 52. comp_out [.FB.]: number of HTTP response bytes emitted by the compressor
110 * 53. comp_byp [.FB.]: number of bytes that bypassed the HTTP compressor (CPU/BW limit)
111 * 54. comp_rsp [.FB.]: number of HTTP responses that were compressed
112 * 55. lastsess [..BS]: number of seconds since last session assigned to server/backend
113 * 56. last_chk [...S]: last health check contents or textual error
114 * 57. last_agt [...S]: last agent check contents or textual error
115 * 58. qtime [..BS]: the average queue time in ms over the 1024 last requests
116 * 59. ctime [..BS]: the average connect time in ms over the 1024 last requests
117 * 60. rtime [..BS]: the average response time in ms over the 1024 last requests (0 for TCP)
118 * 61. ttime [..BS]: the average total session time in ms over the 1024 last requests
119 *
120 * @since 0.1.10
121 */
122class HaproxyStatusParser {
123
124    /**
125     * @var array string column name => string[] rows
126     * Use column name rather than column index to be compatible with future changes in HAProxy.
127     * The array value per column name is a list of all rows values for that column,
128     * e.g. data is organized so they can be extracted by column and row,
129     * not by row and columns as in, for example, a relational database.
130     */
131    private $valuesByColumnName;
132
133    /** @var int */
134    private $numberOfRows;
135
136    /**
137     * @since 0.1.10
138     * @param string $input CSV to be parsed
139     */
140    public function __construct( string $input ) {
141        $values = [];
142        $numberOfRows = 0;
143        $parsedHeaders = false;
144        $rows = str_getcsv( $input, "\n" );
145        $columns = [];
146        foreach ( $rows as $row ) {
147            $csv = str_getcsv( $row );
148            if ( !$parsedHeaders ) {
149                if ( $csv[0] === '# pxname' ) {
150                    $csv[0] = 'pxname';
151                }
152                foreach ( $csv as $columnName ) {
153                    $values[$columnName] = [];
154                    $columns[] = $columnName;
155                }
156                $parsedHeaders = true;
157            } else {
158                foreach ( $csv as $index => $columnValue ) {
159                    // Phan is confused by this $columns[$index] array accessor.
160                    // @phan-suppress-next-line PhanTypeInvalidDimOffset
161                    $values[$columns[$index]][] = $columnValue;
162                }
163                $numberOfRows++;
164            }
165        }
166        $this->valuesByColumnName = $values;
167        $this->numberOfRows = $numberOfRows;
168    }
169
170    /**
171     * @since 0.1.10
172     * @param string $pxname
173     * @param string $svname
174     * @return int
175     * @throws MWException If no such server in parsed data
176     */
177    public function findServerRowIndex(
178        string $pxname,
179        string $svname
180    ): int {
181        for ( $rowIndex = 0; $rowIndex < $this->numberOfRows; $rowIndex++ ) {
182            if (
183                $this->valuesByColumnName['pxname'][$rowIndex] === $pxname
184                && $this->valuesByColumnName['svname'][$rowIndex] === $svname
185            ) {
186                return $rowIndex;
187            }
188        }
189        throw new MWException( "No server defined with pxname '$pxname' and svname '$svname'." );
190    }
191
192    /**
193     * @since 0.1.10
194     * @param string $pxname
195     * @param string $svname
196     * @param string $columnName
197     * @return string values
198     */
199    public function findServerColumnValue(
200        string $pxname,
201        string $svname,
202        string $columnName
203    ): string {
204        return $this->getColumnValue(
205            $this->findServerRowIndex( $pxname, $svname ),
206            $columnName
207        );
208    }
209
210    /**
211     * @since 0.1.10
212     * @param int $rowIndex
213     * @param string $columnName
214     * @return string value
215     */
216    public function getColumnValue(
217        int $rowIndex,
218        string $columnName
219    ): string {
220        return $this->valuesByColumnName[$columnName][$rowIndex];
221    }
222
223    /**
224     * @since 0.1.10
225     * @return int
226     */
227    public function getNumberOfRows(): int {
228        return $this->numberOfRows;
229    }
230
231    /**
232     * Queue is overloaded if there are already the maximum number of current
233     * connections processed by the backend at the same time as the queue
234     * contains more than X connections waiting for their turn,
235     * where X = $overloadedFactor multiplied with
236     * the maximum number of current connections to the backend.
237     *
238     * @since 0.1.10
239     * @param string $frontendPxName
240     * @param string $frontendSvName
241     * @param string $backendPxName
242     * @param string $backendSvName
243     * @param float $overloadedFactor
244     * @return bool Whether or not connection queue is overloaded
245     */
246    public function isQueueOverloaded(
247        string $frontendPxName,
248        string $frontendSvName,
249        string $backendPxName,
250        string $backendSvName,
251        float $overloadedFactor
252    ): bool {
253        $frontendRowIndex = $this->findServerRowIndex( $frontendPxName, $frontendSvName );
254        $frontendCurrentSessions = intval( $this->getColumnValue( $frontendRowIndex, 'scur' ) );
255
256        $backendRowIndex = $this->findServerRowIndex( $backendPxName, $backendSvName );
257        $backendSessionsLimit = $this->getColumnValue( $backendRowIndex, 'slim' );
258        $backendSessionsLimit = intval( $backendSessionsLimit );
259
260        $maximumFrontendCurrentSessions = $backendSessionsLimit * $overloadedFactor;
261
262        return $frontendCurrentSessions >= $maximumFrontendCurrentSessions;
263    }
264
265    /**
266     * Counts number of requests that currently could be sent to the queue
267     * and immediately would be passed down to backend.
268     *
269     * If this value is greater than 0, then the next request sent via the queue
270     * will be immediately processed by the backend.
271     *
272     * If this value is less than 1, then the next connection will be queued,
273     * given that the currently processing requests will not have had time to finish by then.
274     *
275     * If this value is less than 1, then the value is the inverse size of the known queue.
276     * Note that the OS on the HAProxy server might be buffering connections in the TCP-stack
277     * and that HAProxy will not be aware of such connections. A negative number might therefor
278     * not represent a perfect count of current connection lined up in the queue.
279     *
280     * The idea with this function is to see if there are available resources that could
281     * be used for pre-synthesis of utterances during otherwise idle time.
282     *
283     * @since 0.1.10
284     * @param string $frontendPxName
285     * @param string $frontendSvName
286     * @param string $backendPxName
287     * @param string $backendSvName
288     * @return int Positive number if available slots, else inverted size of queue.
289     */
290    public function getAvailableNonQueuedConnectionSlots(
291        string $frontendPxName,
292        string $frontendSvName,
293        string $backendPxName,
294        string $backendSvName
295    ): int {
296        $frontendRowIndex = $this->findServerRowIndex( $frontendPxName, $frontendSvName );
297        $frontendCurrentSessions = intval( $this->getColumnValue( $frontendRowIndex, 'scur' ) );
298
299        $backendRowIndex = $this->findServerRowIndex( $backendPxName, $backendSvName );
300        $backendSessionsLimit = $this->getColumnValue( $backendRowIndex, 'slim' );
301        $backendSessionsLimit = intval( $backendSessionsLimit );
302
303        return $backendSessionsLimit - $frontendCurrentSessions;
304    }
305
306}