Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 404
0.00% covered (danger)
0.00%
0 / 38
CRAP
0.00% covered (danger)
0.00%
0 / 1
MemcachedClient
0.00% covered (danger)
0.00%
0 / 404
0.00% covered (danger)
0.00%
0 / 38
27390
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
2
 serialize
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 unserialize
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 add
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 decr
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 delete
0.00% covered (danger)
0.00%
0 / 20
0.00% covered (danger)
0.00%
0 / 1
90
 touch
0.00% covered (danger)
0.00%
0 / 19
0.00% covered (danger)
0.00%
0 / 1
72
 disconnect_all
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 enable_compress
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 forget_dead_hosts
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 get
0.00% covered (danger)
0.00%
0 / 35
0.00% covered (danger)
0.00%
0 / 1
210
 get_multi
0.00% covered (danger)
0.00%
0 / 37
0.00% covered (danger)
0.00%
0 / 1
210
 incr
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 replace
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 run_command
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
42
 set
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 cas
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 set_compress_threshold
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 set_debug
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 set_servers
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
6
 set_timeout
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 _close_sock
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 _connect_sock
0.00% covered (danger)
0.00%
0 / 24
0.00% covered (danger)
0.00%
0 / 1
72
 _dead_sock
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 _dead_host
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
6
 get_sock
0.00% covered (danger)
0.00%
0 / 22
0.00% covered (danger)
0.00%
0 / 1
132
 _hashfunc
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 _incrdecr
0.00% covered (danger)
0.00%
0 / 23
0.00% covered (danger)
0.00%
0 / 1
72
 _load_items
0.00% covered (danger)
0.00%
0 / 30
0.00% covered (danger)
0.00%
0 / 1
156
 _set
0.00% covered (danger)
0.00%
0 / 46
0.00% covered (danger)
0.00%
0 / 1
420
 sock_to_host
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
72
 _debugprint
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 _error_log
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 _fwrite
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
30
 _handle_error
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
12
 _fread
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
30
 _fgets
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
30
 _flush_read_buffer
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
20
1<?php
2// phpcs:ignoreFile -- It's an external lib and it isn't. Let's not bother.
3/**
4 * Memcached client for PHP.
5 *
6 * +---------------------------------------------------------------------------+
7 * | memcached client, PHP                                                     |
8 * +---------------------------------------------------------------------------+
9 * | Copyright (c) 2003 Ryan T. Dean <rtdean@cytherianage.net>                 |
10 * | All rights reserved.                                                      |
11 * |                                                                           |
12 * | Redistribution and use in source and binary forms, with or without        |
13 * | modification, are permitted provided that the following conditions        |
14 * | are met:                                                                  |
15 * |                                                                           |
16 * | 1. Redistributions of source code must retain the above copyright         |
17 * |    notice, this list of conditions and the following disclaimer.          |
18 * | 2. Redistributions in binary form must reproduce the above copyright      |
19 * |    notice, this list of conditions and the following disclaimer in the    |
20 * |    documentation and/or other materials provided with the distribution.   |
21 * |                                                                           |
22 * | THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR      |
23 * | IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES |
24 * | OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.   |
25 * | IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,          |
26 * | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT  |
27 * | NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
28 * | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY     |
29 * | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT       |
30 * | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF  |
31 * | THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.         |
32 * +---------------------------------------------------------------------------+
33 * | Author: Ryan T. Dean <rtdean@cytherianage.net>                            |
34 * | Heavily influenced by the Perl memcached client by Brad Fitzpatrick.      |
35 * |   Permission granted by Brad Fitzpatrick for relicense of ported Perl     |
36 * |   client logic under 2-clause BSD license.                                |
37 * +---------------------------------------------------------------------------+
38 *
39 * @file
40 * $TCAnet$
41 */
42
43/**
44 * This is a PHP client for memcached - a distributed memory cache daemon.
45 *
46 * More information is available at http://www.danga.com/memcached/
47 *
48 * Usage example:
49 *
50 *     $mc = new MemcachedClient(array(
51 *         'servers' => array(
52 *             '127.0.0.1:10000',
53 *             array( '192.0.0.1:10010', 2 ),
54 *             '127.0.0.1:10020'
55 *         ),
56 *         'debug'   => false,
57 *         'compress_threshold' => 10240,
58 *         'persistent' => true
59 *     ));
60 *
61 *     $mc->add( 'key', array( 'some', 'array' ) );
62 *     $mc->replace( 'key', 'some random string' );
63 *     $val = $mc->get( 'key' );
64 *
65 * @author Ryan T. Dean <rtdean@cytherianage.net>
66 * @version 0.1.2
67 */
68
69use Psr\Log\LoggerInterface;
70use Psr\Log\NullLogger;
71use Wikimedia\AtEase\AtEase;
72use Wikimedia\IPUtils;
73use Wikimedia\LightweightObjectStore\StorageAwareness;
74
75/**
76 * memcached client class implemented using (p)fsockopen()
77 *
78 * @author  Ryan T. Dean <rtdean@cytherianage.net>
79 * @ingroup Cache
80 */
81class MemcachedClient implements StorageAwareness {
82    // {{{ properties
83    // {{{ public
84
85    // {{{ constants
86    // {{{ flags
87
88    /**
89     * Flag: indicates data is serialized
90     */
91    const SERIALIZED = 1;
92
93    /**
94     * Flag: indicates data is compressed
95     */
96    const COMPRESSED = 2;
97
98    /**
99     * Flag: indicates data is an integer
100     */
101    const INTVAL = 4;
102
103    // }}}
104
105    /**
106     * Minimum savings to store data compressed
107     */
108    const COMPRESSION_SAVINGS = 0.20;
109
110    // }}}
111
112    /**
113     * Command statistics
114     *
115     * @var array
116     * @access public
117     */
118    public $stats;
119
120    // }}}
121    // {{{ private
122
123    /**
124     * Cached Sockets that are connected
125     *
126     * @var array
127     * @access private
128     */
129    public $_cache_sock;
130
131    /**
132     * Current debug status; 0 - none to 9 - profiling
133     *
134     * @var bool
135     * @access private
136     */
137    public $_debug;
138
139    /**
140     * Dead hosts, assoc array, 'host'=>'unixtime when ok to check again'
141     *
142     * @var array
143     * @access private
144     */
145    public $_host_dead;
146
147    /**
148     * Is compression available?
149     *
150     * @var bool
151     * @access private
152     */
153    public $_have_zlib;
154
155    /**
156     * Do we want to use compression?
157     *
158     * @var bool
159     * @access private
160     */
161    public $_compress_enable;
162
163    /**
164     * At how many bytes should we compress?
165     *
166     * @var int
167     * @access private
168     */
169    public $_compress_threshold;
170
171    /**
172     * Are we using persistent links?
173     *
174     * @var bool
175     * @access private
176     */
177    public $_persistent;
178
179    /**
180     * If only using one server; contains ip:port to connect to
181     *
182     * @var string
183     * @access private
184     */
185    public $_single_sock;
186
187    /**
188     * Array containing ip:port or array(ip:port, weight)
189     *
190     * @var array
191     * @access private
192     */
193    public $_servers;
194
195    /**
196     * Our bit buckets
197     *
198     * @var array
199     * @access private
200     */
201    public $_buckets;
202
203    /**
204     * Total # of bit buckets we have
205     *
206     * @var int
207     * @access private
208     */
209    public $_bucketcount;
210
211    /**
212     * # of total servers we have
213     *
214     * @var int
215     * @access private
216     */
217    public $_active;
218
219    /**
220     * Stream timeout in seconds. Applies for example to fread()
221     *
222     * @var int
223     * @access private
224     */
225    public $_timeout_seconds;
226
227    /**
228     * Stream timeout in microseconds
229     *
230     * @var int
231     * @access private
232     */
233    public $_timeout_microseconds;
234
235    /**
236     * Connect timeout in seconds
237     */
238    public $_connect_timeout;
239
240    /**
241     * Number of connection attempts for each server
242     */
243    public $_connect_attempts;
244
245    /** @var int StorageAwareness:ERR_* constant of the last cache command */
246    public $_last_cmd_status = self::ERR_NONE;
247
248    /**
249     * @var LoggerInterface
250     */
251    private $_logger;
252
253
254    // }}}
255    // }}}
256    // {{{ methods
257    // {{{ public functions
258    // {{{ memcached()
259
260    /**
261     * Memcache initializer
262     *
263     * @param array $args Associative array of settings
264     */
265    public function __construct( $args ) {
266        $this->set_servers( $args['servers'] ?? array() );
267        $this->_debug = $args['debug'] ?? false;
268        $this->stats = array();
269        $this->_compress_threshold = $args['compress_threshold'] ?? 0;
270        $this->_persistent = $args['persistent'] ?? false;
271        $this->_compress_enable = true;
272        $this->_have_zlib = function_exists( 'gzcompress' );
273
274        $this->_cache_sock = array();
275        $this->_host_dead = array();
276
277        $this->_timeout_seconds = 0;
278        $this->_timeout_microseconds = $args['timeout'] ?? 500_000;
279
280        $this->_connect_timeout = $args['connect_timeout'] ?? 0.1;
281        $this->_connect_attempts = 2;
282
283        $this->_logger = $args['logger'] ?? new NullLogger();
284    }
285
286    // }}}
287
288    /**
289     * @param mixed $value
290     * @return string|integer
291     */
292    public function serialize( $value ) {
293        return serialize( $value );
294    }
295
296    /**
297     * @param string $value
298     * @return mixed
299     */
300    public function unserialize( $value ) {
301        return unserialize( $value );
302    }
303
304    // {{{ add()
305
306    /**
307     * Adds a key/value to the memcache server if one isn't already set with
308     * that key
309     *
310     * @param string $key Key to set with data
311     * @param mixed $val Value to store
312     * @param int $exp (optional) Expiration time. This can be a number of seconds
313     * to cache for (up to 30 days inclusive).  Any timespans of 30 days + 1 second or
314     * longer must be the timestamp of the time at which the mapping should expire. It
315     * is safe to use timestamps in all cases, regardless of expiration
316     * eg: strtotime("+3 hour")
317     *
318     * @return bool
319     */
320    public function add( $key, $val, $exp = 0 ) {
321        $this->_last_cmd_status = self::ERR_NONE;
322
323        return $this->_set( 'add', $key, $val, $exp );
324    }
325
326    // }}}
327    // {{{ decr()
328
329    /**
330     * Decrease a value stored on the memcache server
331     *
332     * @param string $key Key to decrease
333     * @param int $amt (optional) amount to decrease
334     *
335     * @return mixed False on failure, value on success
336     */
337    public function decr( $key, $amt = 1 ) {
338        $this->_last_cmd_status = self::ERR_NONE;
339
340        return $this->_incrdecr( 'decr', $key, $amt );
341    }
342
343    // }}}
344    // {{{ delete()
345
346    /**
347     * Deletes a key from the server, optionally after $time
348     *
349     * @param string $key Key to delete
350     * @param int $time (optional) how long to wait before deleting
351     *
352     * @return bool True on success, false on failure
353     */
354    public function delete( $key, $time = 0 ) {
355        $this->_last_cmd_status = self::ERR_NONE;
356
357        if ( !$this->_active ) {
358            return false;
359        }
360
361        $sock = $this->get_sock( $key );
362        if ( !$sock ) {
363            return false;
364        }
365
366        $key = is_array( $key ) ? $key[1] : $key;
367
368        if ( isset( $this->stats['delete'] ) ) {
369            $this->stats['delete']++;
370        } else {
371            $this->stats['delete'] = 1;
372        }
373        $cmd = "delete $key $time\r\n";
374        if ( !$this->_fwrite( $sock, $cmd ) ) {
375            return false;
376        }
377        $res = $this->_fgets( $sock );
378
379        if ( $this->_debug ) {
380            $this->_debugprint( sprintf( "MemCache: delete %s (%s)", $key, $res ) );
381        }
382
383        if ( $res == "DELETED" || $res == "NOT_FOUND" ) {
384            return true;
385        }
386
387        $this->_last_cmd_status = self::ERR_UNEXPECTED;
388
389        return false;
390    }
391
392    /**
393     * Changes the TTL on a key from the server to $time
394     *
395     * @param string $key
396     * @param int $time TTL in seconds
397     *
398     * @return bool True on success, false on failure
399     */
400    public function touch( $key, $time = 0 ) {
401        $this->_last_cmd_status = self::ERR_NONE;
402
403        if ( !$this->_active ) {
404            return false;
405        }
406
407        $sock = $this->get_sock( $key );
408        if ( !$sock ) {
409            return false;
410        }
411
412        $key = is_array( $key ) ? $key[1] : $key;
413
414        if ( isset( $this->stats['touch'] ) ) {
415            $this->stats['touch']++;
416        } else {
417            $this->stats['touch'] = 1;
418        }
419        $cmd = "touch $key $time\r\n";
420        if ( !$this->_fwrite( $sock, $cmd ) ) {
421            return false;
422        }
423        $res = $this->_fgets( $sock );
424
425        if ( $this->_debug ) {
426            $this->_debugprint( sprintf( "MemCache: touch %s (%s)", $key, $res ) );
427        }
428
429        if ( $res == "TOUCHED" ) {
430            return true;
431        }
432
433        return false;
434    }
435
436    // }}}
437    // {{{ disconnect_all()
438
439    /**
440     * Disconnects all connected sockets
441     */
442    public function disconnect_all() {
443        foreach ( $this->_cache_sock as $sock ) {
444            fclose( $sock );
445        }
446
447        $this->_cache_sock = array();
448    }
449
450    // }}}
451    // {{{ enable_compress()
452
453    /**
454     * Enable / Disable compression
455     *
456     * @param bool $enable True to enable, false to disable
457     */
458    public function enable_compress( $enable ) {
459        $this->_compress_enable = $enable;
460    }
461
462    // }}}
463    // {{{ forget_dead_hosts()
464
465    /**
466     * Forget about all of the dead hosts
467     */
468    public function forget_dead_hosts() {
469        $this->_host_dead = array();
470    }
471
472    // }}}
473    // {{{ get()
474
475    /**
476     * Retrieves the value associated with the key from the memcache server
477     *
478     * @param array|string $key key to retrieve
479     * @param float $casToken [optional]
480     *
481     * @return mixed
482     */
483    public function get( $key, &$casToken = null ) {
484        $getToken = ( func_num_args() >= 2 );
485
486        $this->_last_cmd_status = self::ERR_NONE;
487
488        if ( $this->_debug ) {
489            $this->_debugprint( "get($key)" );
490        }
491
492        if ( !is_array( $key ) && strval( $key ) === '' ) {
493            $this->_last_cmd_status = self::ERR_UNEXPECTED;
494            $this->_debugprint( "Skipping key which equals to an empty string" );
495            return false;
496        }
497
498        if ( !$this->_active ) {
499            $this->_last_cmd_status = self::ERR_UNEXPECTED;
500
501            return false;
502        }
503
504        $sock = $this->get_sock( $key );
505
506        if ( !$sock ) {
507            $this->_last_cmd_status = self::ERR_UNREACHABLE;
508
509            return false;
510        }
511
512        $key = is_array( $key ) ? $key[1] : $key;
513        if ( isset( $this->stats['get'] ) ) {
514            $this->stats['get']++;
515        } else {
516            $this->stats['get'] = 1;
517        }
518
519        $cmd = $getToken ? "gets" : "get";
520        $cmd .= " $key\r\n";
521        if ( !$this->_fwrite( $sock, $cmd ) ) {
522            $this->_last_cmd_status = self::ERR_NO_RESPONSE;
523
524            return false;
525        }
526
527        $val = array();
528        if ( !$this->_load_items( $sock, $val, $casToken ) ) {
529            $this->_last_cmd_status = self::ERR_NO_RESPONSE;
530        }
531
532        if ( $this->_debug ) {
533            foreach ( $val as $k => $v ) {
534                $this->_debugprint(
535                    sprintf( "MemCache: sock %s got %s", $this->serialize( $sock ), $k ) );
536            }
537        }
538
539        $value = false;
540        if ( isset( $val[$key] ) ) {
541            $value = $val[$key];
542        }
543        return $value;
544    }
545
546    // }}}
547    // {{{ get_multi()
548
549    /**
550     * Get multiple keys from the server(s)
551     *
552     * @param array $keys Keys to retrieve
553     *
554     * @return array
555     */
556    public function get_multi( $keys ) {
557        $this->_last_cmd_status = self::ERR_NONE;
558
559        if ( !$this->_active ) {
560            $this->_last_cmd_status = self::ERR_UNEXPECTED;
561
562            return array();
563        }
564
565        if ( isset( $this->stats['get_multi'] ) ) {
566            $this->stats['get_multi']++;
567        } else {
568            $this->stats['get_multi'] = 1;
569        }
570        $sock_keys = array();
571        $socks = array();
572        foreach ( $keys as $key ) {
573            $sock = $this->get_sock( $key );
574            if ( !$sock ) {
575                $this->_last_cmd_status = self::ERR_UNREACHABLE;
576                continue;
577            }
578            $key = is_array( $key ) ? $key[1] : $key;
579            $sockValue = intval( $sock );
580
581            if ( !isset( $sock_keys[$sockValue] ) ) {
582                $sock_keys[$sockValue] = array();
583                $socks[] = $sock;
584            }
585            $sock_keys[$sockValue][] = $key;
586        }
587
588        $gather = array();
589        // Send out the requests
590        foreach ( $socks as $sock ) {
591            $cmd = 'get';
592            foreach ( $sock_keys[intval( $sock )] as $key ) {
593                $cmd .= ' ' . $key;
594            }
595            $cmd .= "\r\n";
596
597            if ( $this->_fwrite( $sock, $cmd ) ) {
598                $gather[] = $sock;
599            } else {
600                $this->_last_cmd_status = self::ERR_NO_RESPONSE;
601            }
602        }
603
604        // Parse responses
605        $val = array();
606        foreach ( $gather as $sock ) {
607            if ( !$this->_load_items( $sock, $val ) ) {
608                $this->_last_cmd_status = self::ERR_NO_RESPONSE;
609            }
610        }
611
612        if ( $this->_debug ) {
613            foreach ( $val as $k => $v ) {
614                $this->_debugprint( sprintf( "MemCache: got %s", $k ) );
615            }
616        }
617
618        return $val;
619    }
620
621    // }}}
622    // {{{ incr()
623
624    /**
625     * Increments $key (optionally) by $amt
626     *
627     * @param string $key Key to increment
628     * @param int $amt (optional) amount to increment
629     *
630     * @return int|null Null if the key does not exist yet (this does NOT
631     * create new mappings if the key does not exist). If the key does
632     * exist, this returns the new value for that key.
633     */
634    public function incr( $key, $amt = 1 ) {
635        return $this->_incrdecr( 'incr', $key, $amt );
636    }
637
638    // }}}
639    // {{{ replace()
640
641    /**
642     * Overwrites an existing value for key; only works if key is already set
643     *
644     * @param string $key Key to set value as
645     * @param mixed $value Value to store
646     * @param int $exp (optional) Expiration time. This can be a number of seconds
647     * to cache for (up to 30 days inclusive).  Any timespans of 30 days + 1 second or
648     * longer must be the timestamp of the time at which the mapping should expire. It
649     * is safe to use timestamps in all cases, regardless of expiration
650     * eg: strtotime("+3 hour")
651     *
652     * @return bool
653     */
654    public function replace( $key, $value, $exp = 0 ) {
655        return $this->_set( 'replace', $key, $value, $exp );
656    }
657
658    // }}}
659    // {{{ run_command()
660
661    /**
662     * Passes through $cmd to the memcache server connected by $sock; returns
663     * output as an array (null array if no output)
664     *
665     * @param Resource $sock Socket to send command on
666     * @param string $cmd Command to run
667     *
668     * @return array Output array
669     */
670    public function run_command( $sock, $cmd ) {
671        if ( !$sock ) {
672            return array();
673        }
674
675        if ( !$this->_fwrite( $sock, $cmd ) ) {
676            return array();
677        }
678
679        $ret = array();
680        while ( true ) {
681            $res = $this->_fgets( $sock );
682            $ret[] = $res;
683            if ( preg_match( '/^END/', $res ) ) {
684                break;
685            }
686            if ( strlen( $res ) == 0 ) {
687                break;
688            }
689        }
690        return $ret;
691    }
692
693    // }}}
694    // {{{ set()
695
696    /**
697     * Unconditionally sets a key to a given value in the memcache.  Returns true
698     * if set successfully.
699     *
700     * @param string $key Key to set value as
701     * @param mixed $value Value to set
702     * @param int $exp (optional) Expiration time. This can be a number of seconds
703     * to cache for (up to 30 days inclusive).  Any timespans of 30 days + 1 second or
704     * longer must be the timestamp of the time at which the mapping should expire. It
705     * is safe to use timestamps in all cases, regardless of expiration
706     * eg: strtotime("+3 hour")
707     *
708     * @return bool True on success
709     */
710    public function set( $key, $value, $exp = 0 ) {
711        return $this->_set( 'set', $key, $value, $exp );
712    }
713
714    // }}}
715    // {{{ cas()
716
717    /**
718     * Sets a key to a given value in the memcache if the current value still corresponds
719     * to a known, given value.  Returns true if set successfully.
720     *
721     * @param float $casToken Current known value
722     * @param string $key Key to set value as
723     * @param mixed $value Value to set
724     * @param int $exp (optional) Expiration time. This can be a number of seconds
725     * to cache for (up to 30 days inclusive).  Any timespans of 30 days + 1 second or
726     * longer must be the timestamp of the time at which the mapping should expire. It
727     * is safe to use timestamps in all cases, regardless of expiration
728     * eg: strtotime("+3 hour")
729     *
730     * @return bool True on success
731     */
732    public function cas( $casToken, $key, $value, $exp = 0 ) {
733        return $this->_set( 'cas', $key, $value, $exp, $casToken );
734    }
735
736    // }}}
737    // {{{ set_compress_threshold()
738
739    /**
740     * Set the compression threshold
741     *
742     * @param int $thresh Threshold to compress if larger than
743     */
744    public function set_compress_threshold( $thresh ) {
745        $this->_compress_threshold = $thresh;
746    }
747
748    // }}}
749    // {{{ set_debug()
750
751    /**
752     * Set the debug flag
753     *
754     * @see __construct()
755     * @param bool $dbg True for debugging, false otherwise
756     */
757    public function set_debug( $dbg ) {
758        $this->_debug = $dbg;
759    }
760
761    // }}}
762    // {{{ set_servers()
763
764    /**
765     * Set the server list to distribute key gets and puts between
766     *
767     * @see __construct()
768     * @param array $list Array of servers to connect to
769     */
770    public function set_servers( $list ) {
771        $this->_servers = $list;
772        $this->_active = count( $list );
773        $this->_buckets = null;
774        $this->_bucketcount = 0;
775
776        $this->_single_sock = null;
777        if ( $this->_active == 1 ) {
778            $this->_single_sock = $this->_servers[0];
779        }
780    }
781
782    /**
783     * Sets the timeout for new connections
784     *
785     * @param int $seconds Number of seconds
786     * @param int $microseconds Number of microseconds
787     */
788    public function set_timeout( $seconds, $microseconds ) {
789        $this->_timeout_seconds = $seconds;
790        $this->_timeout_microseconds = $microseconds;
791    }
792
793    // }}}
794    // }}}
795    // {{{ private methods
796    // {{{ _close_sock()
797
798    /**
799     * Close the specified socket
800     *
801     * @param string $sock Socket to close
802     *
803     * @access private
804     */
805    function _close_sock( $sock ) {
806        $host = array_search( $sock, $this->_cache_sock );
807        fclose( $this->_cache_sock[$host] );
808        unset( $this->_cache_sock[$host] );
809    }
810
811    // }}}
812    // {{{ _connect_sock()
813
814    /**
815     * Connects $sock to $host, timing out after $timeout
816     *
817     * @param int $sock Socket to connect
818     * @param string $host Host:IP to connect to
819     *
820     * @return bool
821     * @access private
822     */
823    function _connect_sock( &$sock, $host ) {
824        $port = null;
825        $hostAndPort = IPUtils::splitHostAndPort( $host );
826        if ( $hostAndPort ) {
827            $ip = $hostAndPort[0];
828            if ( $hostAndPort[1] ) {
829                $port = $hostAndPort[1];
830            }
831        } else {
832            $ip = $host;
833        }
834        $sock = false;
835        $timeout = $this->_connect_timeout;
836        $errno = $errstr = null;
837        for ( $i = 0; !$sock && $i < $this->_connect_attempts; $i++ ) {
838            AtEase::suppressWarnings();
839            if ( $this->_persistent == 1 ) {
840                $sock = pfsockopen( $ip, $port, $errno, $errstr, $timeout );
841            } else {
842                $sock = fsockopen( $ip, $port, $errno, $errstr, $timeout );
843            }
844            AtEase::restoreWarnings();
845        }
846        if ( !$sock ) {
847            $this->_error_log( "Error connecting to $host$errstr" );
848            $this->_dead_host( $host );
849            return false;
850        }
851
852        // Initialise timeout
853        stream_set_timeout( $sock, $this->_timeout_seconds, $this->_timeout_microseconds );
854
855        // If the connection was persistent, flush the read buffer in case there
856        // was a previous incomplete request on this connection
857        if ( $this->_persistent ) {
858            $this->_flush_read_buffer( $sock );
859        }
860        return true;
861    }
862
863    // }}}
864    // {{{ _dead_sock()
865
866    /**
867     * Marks a host as dead until 30-40 seconds in the future
868     *
869     * @param string $sock Socket to mark as dead
870     *
871     * @access private
872     */
873    function _dead_sock( $sock ) {
874        $host = array_search( $sock, $this->_cache_sock );
875        $this->_dead_host( $host );
876    }
877
878    /**
879     * @param string $host
880     */
881    function _dead_host( $host ) {
882        $hostAndPort = IPUtils::splitHostAndPort( $host );
883        if ( $hostAndPort ) {
884            $ip = $hostAndPort[0];
885        } else {
886            $ip = $host;
887        }
888        $this->_host_dead[$ip] = time() + 30 + intval( rand( 0, 10 ) );
889        $this->_host_dead[$host] = $this->_host_dead[$ip];
890        unset( $this->_cache_sock[$host] );
891    }
892
893    // }}}
894    // {{{ get_sock()
895
896    /**
897     * get_sock
898     *
899     * @param string $key Key to retrieve value for;
900     *
901     * @return Resource|bool Resource on success, false on failure
902     * @access private
903     */
904    function get_sock( $key ) {
905        if ( !$this->_active ) {
906            return false;
907        }
908
909        if ( $this->_single_sock !== null ) {
910            return $this->sock_to_host( $this->_single_sock );
911        }
912
913        $hv = is_array( $key ) ? intval( $key[0] ) : $this->_hashfunc( $key );
914        if ( $this->_buckets === null ) {
915            $bu = array();
916            foreach ( $this->_servers as $v ) {
917                if ( is_array( $v ) ) {
918                    for ( $i = 0; $i < $v[1]; $i++ ) {
919                        $bu[] = $v[0];
920                    }
921                } else {
922                    $bu[] = $v;
923                }
924            }
925            $this->_buckets = $bu;
926            $this->_bucketcount = count( $bu );
927        }
928
929        $realkey = is_array( $key ) ? $key[1] : $key;
930        for ( $tries = 0; $tries < 20; $tries++ ) {
931            $host = $this->_buckets[$hv % $this->_bucketcount];
932            $sock = $this->sock_to_host( $host );
933            if ( $sock ) {
934                return $sock;
935            }
936            $hv = $this->_hashfunc( $hv . $realkey );
937        }
938
939        return false;
940    }
941
942    // }}}
943    // {{{ _hashfunc()
944
945    /**
946     * Creates a hash integer based on the $key
947     *
948     * @param string $key Key to hash
949     *
950     * @return int Hash value
951     * @access private
952     */
953    function _hashfunc( $key ) {
954        # Hash function must be in [0,0x7ffffff]
955        # We take the first 31 bits of the MD5 hash, which unlike the hash
956        # function used in a previous version of this client, works
957        return hexdec( substr( md5( $key ), 0, 8 ) ) & 0x7fffffff;
958    }
959
960    // }}}
961    // {{{ _incrdecr()
962
963    /**
964     * Perform increment/decrement on $key
965     *
966     * @param string $cmd Command to perform
967     * @param string|array $key Key to perform it on
968     * @param int $amt Amount to adjust
969     *
970     * @return int New value of $key
971     * @access private
972     */
973    function _incrdecr( $cmd, $key, $amt = 1 ) {
974        $this->_last_cmd_status = self::ERR_NONE;
975
976        if ( !$this->_active ) {
977            $this->_last_cmd_status = self::ERR_UNEXPECTED;
978
979            return null;
980        }
981
982        $sock = $this->get_sock( $key );
983        if ( !$sock ) {
984            $this->_last_cmd_status = self::ERR_UNREACHABLE;
985
986            return null;
987        }
988
989        $key = is_array( $key ) ? $key[1] : $key;
990        if ( isset( $this->stats[$cmd] ) ) {
991            $this->stats[$cmd]++;
992        } else {
993            $this->stats[$cmd] = 1;
994        }
995        if ( !$this->_fwrite( $sock, "$cmd $key $amt\r\n" ) ) {
996            $this->_last_cmd_status = self::ERR_NO_RESPONSE;
997
998            return null;
999        }
1000
1001        $line = $this->_fgets( $sock );
1002        if ( $this->_debug ) {
1003            $this->_debugprint( "$cmd($key): $line" );
1004        }
1005
1006        $match = array();
1007        if ( !preg_match( '/^(\d+)/', $line, $match ) ) {
1008            $this->_last_cmd_status = self::ERR_NO_RESPONSE;
1009
1010            return null;
1011        }
1012
1013        return (int)$match[1];
1014    }
1015
1016    // }}}
1017    // {{{ _load_items()
1018
1019    /**
1020     * Load items into $ret from $sock
1021     *
1022     * @param Resource $sock Socket to read from
1023     * @param array $ret returned values
1024     * @param float $casToken [optional]
1025     * @return bool True for success, false for failure
1026     *
1027     * @access private
1028     */
1029    function _load_items( $sock, &$ret, &$casToken = null ) {
1030        $results = array();
1031
1032        while ( 1 ) {
1033            $decl = $this->_fgets( $sock );
1034
1035            if ( $decl === false ) {
1036                /*
1037                 * If nothing can be read, something is wrong because we know exactly when
1038                 * to stop reading (right after "END") and we return right after that.
1039                 */
1040                return false;
1041            } elseif ( preg_match( '/^VALUE (\S+) (\d+) (\d+)(?: (\d+))?$/', $decl, $match ) ) {
1042                /*
1043                 * Read all data returned. This can be either one or multiple values.
1044                 * Save all that data (in an array) to be processed later: we'll first
1045                 * want to continue reading until "END" before doing anything else,
1046                 * to make sure that we don't leave our client in a state where it's
1047                 * output is not yet fully read.
1048                 */
1049                $results[] = array(
1050                    $match[1], // rkey
1051                    $match[2], // flags
1052                    $match[3], // len
1053                    $match[4] ?? null, // casToken (appears with "gets" but not "get")
1054                    $this->_fread( $sock, $match[3] + 2 ), // data
1055                );
1056            } elseif ( $decl == "END" ) {
1057                /**
1058                 * All data has been read, time to process the data and build
1059                 * meaningful return values.
1060                 */
1061                foreach ( $results as [ $rkey, $flags, /* length */, $casToken, $data ] ) {
1062                    if ( $data === false || substr( $data, -2 ) !== "\r\n" ) {
1063                        $this->_handle_error( $sock,
1064                            'line ending missing from data block from $1' );
1065                        return false;
1066                    }
1067                    $data = substr( $data, 0, -2 );
1068                    $ret[$rkey] = $data;
1069
1070                    if ( $this->_have_zlib && $flags & self::COMPRESSED ) {
1071                        $ret[$rkey] = gzuncompress( $ret[$rkey] );
1072                    }
1073
1074                    /*
1075                     * This unserialize is the exact reason that we only want to
1076                     * process data after having read until "END" (instead of doing
1077                     * this right away): "unserialize" can trigger outside code:
1078                     * in the event that $ret[$rkey] is a serialized object,
1079                     * unserializing it will trigger __wakeup() if present. If that
1080                     * function attempted to read from memcached (while we did not
1081                     * yet read "END"), these 2 calls would collide.
1082                     */
1083                    if ( $flags & self::SERIALIZED ) {
1084                        $ret[$rkey] = $this->unserialize( $ret[$rkey] );
1085                    } elseif ( $flags & self::INTVAL ) {
1086                        $ret[$rkey] = intval( $ret[$rkey] );
1087                    }
1088                }
1089
1090                return true;
1091            } else {
1092                $this->_handle_error( $sock, 'Error parsing response from $1' );
1093                return false;
1094            }
1095        }
1096    }
1097
1098    // }}}
1099    // {{{ _set()
1100
1101    /**
1102     * Performs the requested storage operation to the memcache server
1103     *
1104     * @param string $cmd Command to perform
1105     * @param string $key Key to act on
1106     * @param mixed $val What we need to store
1107     * @param int $exp (optional) Expiration time. This can be a number of seconds
1108     * to cache for (up to 30 days inclusive).  Any timespans of 30 days + 1 second or
1109     * longer must be the timestamp of the time at which the mapping should expire. It
1110     * is safe to use timestamps in all cases, regardless of expiration
1111     * eg: strtotime("+3 hour")
1112     * @param float $casToken [optional]
1113     *
1114     * @return bool
1115     * @access private
1116     */
1117    function _set( $cmd, $key, $val, $exp, $casToken = null ) {
1118        $this->_last_cmd_status = self::ERR_NONE;
1119
1120        if ( !$this->_active ) {
1121            $this->_last_cmd_status = self::ERR_UNEXPECTED;
1122
1123            return false;
1124        }
1125
1126        $sock = $this->get_sock( $key );
1127        if ( !$sock ) {
1128            $this->_last_cmd_status = self::ERR_UNREACHABLE;
1129
1130            return false;
1131        }
1132
1133        if ( isset( $this->stats[$cmd] ) ) {
1134            $this->stats[$cmd]++;
1135        } else {
1136            $this->stats[$cmd] = 1;
1137        }
1138
1139        $flags = 0;
1140
1141        if ( is_int( $val ) ) {
1142            $flags |= self::INTVAL;
1143        } elseif ( !is_scalar( $val ) ) {
1144            $val = $this->serialize( $val );
1145            $flags |= self::SERIALIZED;
1146            if ( $this->_debug ) {
1147                $this->_debugprint( "client: serializing data as it is not scalar" );
1148            }
1149        }
1150
1151        $len = strlen( $val );
1152
1153        if ( $this->_have_zlib && $this->_compress_enable
1154            && $this->_compress_threshold && $len >= $this->_compress_threshold
1155        ) {
1156            $c_val = gzcompress( $val, 9 );
1157            $c_len = strlen( $c_val );
1158
1159            if ( $c_len < $len * ( 1 - self::COMPRESSION_SAVINGS ) ) {
1160                if ( $this->_debug ) {
1161                    $this->_debugprint( sprintf( "client: compressing data; was %d bytes is now %d bytes", $len, $c_len ) );
1162                }
1163                $val = $c_val;
1164                $len = $c_len;
1165                $flags |= self::COMPRESSED;
1166            }
1167        }
1168
1169        $command = "$cmd $key $flags $exp $len";
1170        if ( $casToken ) {
1171            $command .= " $casToken";
1172        }
1173
1174        if ( !$this->_fwrite( $sock, "$command\r\n$val\r\n" ) ) {
1175            $this->_last_cmd_status = self::ERR_NO_RESPONSE;
1176
1177            return false;
1178        }
1179
1180        $line = $this->_fgets( $sock );
1181        if ( $this->_debug ) {
1182            $this->_debugprint( sprintf( "%s %s (%s)", $cmd, $key, $line ) );
1183        }
1184
1185        if ( $line === "STORED" ) {
1186            return true;
1187        } elseif ( $line === "NOT_STORED" && $cmd === "set" ) {
1188            // "Not stored" is always used as the mcrouter response with AllAsyncRoute
1189            return true;
1190        }
1191
1192        if ( $line === false ) {
1193            $this->_last_cmd_status = self::ERR_NO_RESPONSE;
1194        }
1195
1196        return false;
1197    }
1198
1199    // }}}
1200    // {{{ sock_to_host()
1201
1202    /**
1203     * Returns the socket for the host
1204     *
1205     * @param string $host Host:IP to get socket for
1206     *
1207     * @return Resource|bool IO Stream or false
1208     * @access private
1209     */
1210    function sock_to_host( $host ) {
1211        if ( isset( $this->_cache_sock[$host] ) ) {
1212            return $this->_cache_sock[$host];
1213        }
1214
1215        $sock = null;
1216        $now = time();
1217        $hostAndPort = IPUtils::splitHostAndPort( $host );
1218        if ( $hostAndPort ) {
1219            $ip = $hostAndPort[0];
1220        } else {
1221            $ip = $host;
1222        }
1223        if ( isset( $this->_host_dead[$host] ) && $this->_host_dead[$host] > $now ||
1224            isset( $this->_host_dead[$ip] ) && $this->_host_dead[$ip] > $now
1225        ) {
1226            return null;
1227        }
1228
1229        if ( !$this->_connect_sock( $sock, $host ) ) {
1230            return null;
1231        }
1232
1233        // Do not buffer writes
1234        stream_set_write_buffer( $sock, 0 );
1235
1236        $this->_cache_sock[$host] = $sock;
1237
1238        return $this->_cache_sock[$host];
1239    }
1240
1241    /**
1242     * @param string $text
1243     */
1244    function _debugprint( $text ) {
1245        $this->_logger->debug( $text );
1246    }
1247
1248    /**
1249     * @param string $text
1250     */
1251    function _error_log( $text ) {
1252        $this->_logger->error( "Memcached error: $text" );
1253    }
1254
1255    /**
1256     * Write to a stream. If there is an error, mark the socket dead.
1257     *
1258     * @param Resource $sock The socket
1259     * @param string $buf The string to write
1260     * @return bool True on success, false on failure
1261     */
1262    function _fwrite( $sock, $buf ) {
1263        $bytesWritten = 0;
1264        $bufSize = strlen( $buf );
1265        while ( $bytesWritten < $bufSize ) {
1266            $result = fwrite( $sock, $buf );
1267            $data = stream_get_meta_data( $sock );
1268            if ( $data['timed_out'] ) {
1269                $this->_handle_error( $sock, 'timeout writing to $1' );
1270                return false;
1271            }
1272            // Contrary to the documentation, fwrite() returns zero on error in PHP 5.3.
1273            if ( $result === false || $result === 0 ) {
1274                $this->_handle_error( $sock, 'error writing to $1' );
1275                return false;
1276            }
1277            $bytesWritten += $result;
1278        }
1279
1280        return true;
1281    }
1282
1283    /**
1284     * Handle an I/O error. Mark the socket dead and log an error.
1285     *
1286     * @param Resource $sock
1287     * @param string $msg
1288     */
1289    function _handle_error( $sock, $msg ) {
1290        $peer = stream_socket_get_name( $sock, true /** remote **/ );
1291        if ( strval( $peer ) === '' ) {
1292            $peer = array_search( $sock, $this->_cache_sock );
1293            if ( $peer === false ) {
1294                $peer = '[unknown host]';
1295            }
1296        }
1297        $msg = str_replace( '$1', $peer, $msg );
1298        $this->_error_log( "$msg" );
1299        $this->_dead_sock( $sock );
1300    }
1301
1302    /**
1303     * Read the specified number of bytes from a stream. If there is an error,
1304     * mark the socket dead.
1305     *
1306     * @param Resource $sock The socket
1307     * @param int $len The number of bytes to read
1308     * @return string|bool The string on success, false on failure.
1309     */
1310    function _fread( $sock, $len ) {
1311        $buf = '';
1312        while ( $len > 0 ) {
1313            $result = fread( $sock, $len );
1314            $data = stream_get_meta_data( $sock );
1315            if ( $data['timed_out'] ) {
1316                $this->_handle_error( $sock, 'timeout reading from $1' );
1317                return false;
1318            }
1319            if ( $result === false ) {
1320                $this->_handle_error( $sock, 'error reading buffer from $1' );
1321                return false;
1322            }
1323            if ( $result === '' ) {
1324                // This will happen if the remote end of the socket is shut down
1325                $this->_handle_error( $sock, 'unexpected end of file reading from $1' );
1326                return false;
1327            }
1328            $len -= strlen( $result );
1329            $buf .= $result;
1330        }
1331        return $buf;
1332    }
1333
1334    /**
1335     * Read a line from a stream. If there is an error, mark the socket dead.
1336     * The \r\n line ending is stripped from the response.
1337     *
1338     * @param Resource $sock The socket
1339     * @return string|bool The string on success, false on failure
1340     */
1341    function _fgets( $sock ) {
1342        $result = fgets( $sock );
1343        // fgets() may return a partial line if there is a select timeout after
1344        // a successful recv(), so we have to check for a timeout even if we
1345        // got a string response.
1346        $data = stream_get_meta_data( $sock );
1347        if ( $data['timed_out'] ) {
1348            $this->_handle_error( $sock, 'timeout reading line from $1' );
1349            return false;
1350        }
1351        if ( $result === false ) {
1352            $this->_handle_error( $sock, 'error reading line from $1' );
1353            return false;
1354        }
1355        if ( substr( $result, -2 ) === "\r\n" ) {
1356            $result = substr( $result, 0, -2 );
1357        } elseif ( substr( $result, -1 ) === "\n" ) {
1358            $result = substr( $result, 0, -1 );
1359        } else {
1360            $this->_handle_error( $sock, 'line ending missing in response from $1' );
1361            return false;
1362        }
1363        return $result;
1364    }
1365
1366    /**
1367     * Flush the read buffer of a stream
1368     * @param Resource $f
1369     */
1370    function _flush_read_buffer( $f ) {
1371        if ( !$f ) {
1372            return;
1373        }
1374        $r = array( $f );
1375        $w = null;
1376        $e = null;
1377        $n = stream_select( $r, $w, $e, 0, 0 );
1378        while ( $n == 1 && !feof( $f ) ) {
1379            fread( $f, 1024 );
1380            $r = array( $f );
1381            $w = null;
1382            $e = null;
1383            $n = stream_select( $r, $w, $e, 0, 0 );
1384        }
1385    }
1386
1387    // }}}
1388    // }}}
1389    // }}}
1390}