Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
66.67% covered (warning)
66.67%
30 / 45
42.86% covered (danger)
42.86%
3 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
HaproxyStatusParser
66.67% covered (warning)
66.67%
30 / 45
42.86% covered (danger)
42.86%
3 / 7
25.48
0.00% covered (danger)
0.00%
0 / 1
 __construct
95.24% covered (success)
95.24%
20 / 21
0.00% covered (danger)
0.00%
0 / 1
7
 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 RuntimeException;
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", enclosure: '"', escape: '' );
145        $columns = [];
146        foreach ( $rows as $row ) {
147            if ( !$row ) {
148                continue;
149            }
150
151            $csv = str_getcsv( $row, separator: ',', enclosure: '"', escape: '' );
152            if ( !$parsedHeaders ) {
153                if ( $csv[0] === '# pxname' ) {
154                    $csv[0] = 'pxname';
155                }
156                foreach ( $csv as $columnName ) {
157                    $values[$columnName] = [];
158                    $columns[] = $columnName;
159                }
160                $parsedHeaders = true;
161            } else {
162                foreach ( $csv as $index => $columnValue ) {
163                    // Phan is confused by this $columns[$index] array accessor.
164                    // @phan-suppress-next-line PhanTypeInvalidDimOffset
165                    $values[$columns[$index]][] = $columnValue;
166                }
167                $numberOfRows++;
168            }
169        }
170        $this->valuesByColumnName = $values;
171        $this->numberOfRows = $numberOfRows;
172    }
173
174    /**
175     * @since 0.1.10
176     * @param string $pxname
177     * @param string $svname
178     * @return int
179     * @throws RuntimeException If no such server in parsed data
180     */
181    public function findServerRowIndex(
182        string $pxname,
183        string $svname
184    ): int {
185        for ( $rowIndex = 0; $rowIndex < $this->numberOfRows; $rowIndex++ ) {
186            if (
187                $this->valuesByColumnName['pxname'][$rowIndex] === $pxname
188                && $this->valuesByColumnName['svname'][$rowIndex] === $svname
189            ) {
190                return $rowIndex;
191            }
192        }
193        throw new RuntimeException( "No server defined with pxname '$pxname' and svname '$svname'." );
194    }
195
196    /**
197     * @since 0.1.10
198     * @param string $pxname
199     * @param string $svname
200     * @param string $columnName
201     * @return string values
202     */
203    public function findServerColumnValue(
204        string $pxname,
205        string $svname,
206        string $columnName
207    ): string {
208        return $this->getColumnValue(
209            $this->findServerRowIndex( $pxname, $svname ),
210            $columnName
211        );
212    }
213
214    /**
215     * @since 0.1.10
216     * @param int $rowIndex
217     * @param string $columnName
218     * @return string value
219     */
220    public function getColumnValue(
221        int $rowIndex,
222        string $columnName
223    ): string {
224        return $this->valuesByColumnName[$columnName][$rowIndex];
225    }
226
227    /**
228     * @since 0.1.10
229     * @return int
230     */
231    public function getNumberOfRows(): int {
232        return $this->numberOfRows;
233    }
234
235    /**
236     * Queue is overloaded if there are already the maximum number of current
237     * connections processed by the backend at the same time as the queue
238     * contains more than X connections waiting for their turn,
239     * where X = $overloadedFactor multiplied with
240     * the maximum number of current connections to the backend.
241     *
242     * @since 0.1.10
243     * @param string $frontendPxName
244     * @param string $frontendSvName
245     * @param string $backendPxName
246     * @param string $backendSvName
247     * @param float $overloadedFactor
248     * @return bool Whether or not connection queue is overloaded
249     */
250    public function isQueueOverloaded(
251        string $frontendPxName,
252        string $frontendSvName,
253        string $backendPxName,
254        string $backendSvName,
255        float $overloadedFactor
256    ): bool {
257        $frontendRowIndex = $this->findServerRowIndex( $frontendPxName, $frontendSvName );
258        $frontendCurrentSessions = intval( $this->getColumnValue( $frontendRowIndex, 'scur' ) );
259
260        $backendRowIndex = $this->findServerRowIndex( $backendPxName, $backendSvName );
261        $backendSessionsLimit = $this->getColumnValue( $backendRowIndex, 'slim' );
262        $backendSessionsLimit = intval( $backendSessionsLimit );
263
264        $maximumFrontendCurrentSessions = $backendSessionsLimit * $overloadedFactor;
265
266        return $frontendCurrentSessions >= $maximumFrontendCurrentSessions;
267    }
268
269    /**
270     * Counts number of requests that currently could be sent to the queue
271     * and immediately would be passed down to backend.
272     *
273     * If this value is greater than 0, then the next request sent via the queue
274     * will be immediately processed by the backend.
275     *
276     * If this value is less than 1, then the next connection will be queued,
277     * given that the currently processing requests will not have had time to finish by then.
278     *
279     * If this value is less than 1, then the value is the inverse size of the known queue.
280     * Note that the OS on the HAProxy server might be buffering connections in the TCP-stack
281     * and that HAProxy will not be aware of such connections. A negative number might therefor
282     * not represent a perfect count of current connection lined up in the queue.
283     *
284     * The idea with this function is to see if there are available resources that could
285     * be used for pre-synthesis of utterances during otherwise idle time.
286     *
287     * @since 0.1.10
288     * @param string $frontendPxName
289     * @param string $frontendSvName
290     * @param string $backendPxName
291     * @param string $backendSvName
292     * @return int Positive number if available slots, else inverted size of queue.
293     */
294    public function getAvailableNonQueuedConnectionSlots(
295        string $frontendPxName,
296        string $frontendSvName,
297        string $backendPxName,
298        string $backendSvName
299    ): int {
300        $frontendRowIndex = $this->findServerRowIndex( $frontendPxName, $frontendSvName );
301        $frontendCurrentSessions = intval( $this->getColumnValue( $frontendRowIndex, 'scur' ) );
302
303        $backendRowIndex = $this->findServerRowIndex( $backendPxName, $backendSvName );
304        $backendSessionsLimit = $this->getColumnValue( $backendRowIndex, 'slim' );
305        $backendSessionsLimit = intval( $backendSessionsLimit );
306
307        return $backendSessionsLimit - $frontendCurrentSessions;
308    }
309
310}