MediaWiki REL1_32
UIDGenerator.php
Go to the documentation of this file.
1<?php
22use Wikimedia\Assert\Assert;
24
32 protected static $instance = null;
34 protected $nodeIdFile;
36 protected $nodeId32;
38 protected $nodeId48;
39
41 protected $lockFile88;
43 protected $lockFile128;
45 protected $lockFileUUID;
46
48 protected $fileHandles = []; // cached file handles
49
50 const QUICK_RAND = 1; // get randomness from fast and insecure sources
51 const QUICK_VOLATILE = 2; // use an APC like in-memory counter if available
52
53 protected function __construct() {
54 $this->nodeIdFile = wfTempDir() . '/mw-' . __CLASS__ . '-UID-nodeid';
55 $nodeId = '';
56 if ( is_file( $this->nodeIdFile ) ) {
57 $nodeId = file_get_contents( $this->nodeIdFile );
58 }
59 // Try to get some ID that uniquely identifies this machine (RFC 4122)...
60 if ( !preg_match( '/^[0-9a-f]{12}$/i', $nodeId ) ) {
61 Wikimedia\suppressWarnings();
62 if ( wfIsWindows() ) {
63 // https://technet.microsoft.com/en-us/library/bb490913.aspx
64 $csv = trim( wfShellExec( 'getmac /NH /FO CSV' ) );
65 $line = substr( $csv, 0, strcspn( $csv, "\n" ) );
66 $info = str_getcsv( $line );
67 $nodeId = isset( $info[0] ) ? str_replace( '-', '', $info[0] ) : '';
68 } elseif ( is_executable( '/sbin/ifconfig' ) ) { // Linux/BSD/Solaris/OS X
69 // See https://linux.die.net/man/8/ifconfig
70 $m = [];
71 preg_match( '/\s([0-9a-f]{2}(:[0-9a-f]{2}){5})\s/',
72 wfShellExec( '/sbin/ifconfig -a' ), $m );
73 $nodeId = isset( $m[1] ) ? str_replace( ':', '', $m[1] ) : '';
74 }
75 Wikimedia\restoreWarnings();
76 if ( !preg_match( '/^[0-9a-f]{12}$/i', $nodeId ) ) {
77 $nodeId = MWCryptRand::generateHex( 12, true );
78 $nodeId[1] = dechex( hexdec( $nodeId[1] ) | 0x1 ); // set multicast bit
79 }
80 file_put_contents( $this->nodeIdFile, $nodeId ); // cache
81 }
82 $this->nodeId32 = Wikimedia\base_convert( substr( sha1( $nodeId ), 0, 8 ), 16, 2, 32 );
83 $this->nodeId48 = Wikimedia\base_convert( $nodeId, 16, 2, 48 );
84 // If different processes run as different users, they may have different temp dirs.
85 // This is dealt with by initializing the clock sequence number and counters randomly.
86 $this->lockFile88 = wfTempDir() . '/mw-' . __CLASS__ . '-UID-88';
87 $this->lockFile128 = wfTempDir() . '/mw-' . __CLASS__ . '-UID-128';
88 $this->lockFileUUID = wfTempDir() . '/mw-' . __CLASS__ . '-UUID-128';
89 }
90
95 protected static function singleton() {
96 if ( self::$instance === null ) {
97 self::$instance = new self();
98 }
99
100 return self::$instance;
101 }
102
118 public static function newTimestampedUID88( $base = 10 ) {
119 Assert::parameterType( 'integer', $base, '$base' );
120 Assert::parameter( $base <= 36, '$base', 'must be <= 36' );
121 Assert::parameter( $base >= 2, '$base', 'must be >= 2' );
122
123 $gen = self::singleton();
124 $info = $gen->getTimeAndDelay( 'lockFile88', 1, 1024, 1024 );
125 $info['offsetCounter'] = $info['offsetCounter'] % 1024;
126 return Wikimedia\base_convert( $gen->getTimestampedID88( $info ), 2, $base );
127 }
128
135 protected function getTimestampedID88( array $info ) {
136 if ( isset( $info['time'] ) ) {
137 $time = $info['time'];
138 $counter = $info['offsetCounter'];
139 } else {
140 $time = $info[0];
141 $counter = $info[1];
142 }
143 // Take the 46 LSBs of "milliseconds since epoch"
144 $id_bin = $this->millisecondsSinceEpochBinary( $time );
145 // Add a 10 bit counter resulting in 56 bits total
146 $id_bin .= str_pad( decbin( $counter ), 10, '0', STR_PAD_LEFT );
147 // Add the 32 bit node ID resulting in 88 bits total
148 $id_bin .= $this->nodeId32;
149 // Convert to a 1-27 digit integer string
150 if ( strlen( $id_bin ) !== 88 ) {
151 throw new RuntimeException( "Detected overflow for millisecond timestamp." );
152 }
153
154 return $id_bin;
155 }
156
171 public static function newTimestampedUID128( $base = 10 ) {
172 Assert::parameterType( 'integer', $base, '$base' );
173 Assert::parameter( $base <= 36, '$base', 'must be <= 36' );
174 Assert::parameter( $base >= 2, '$base', 'must be >= 2' );
175
176 $gen = self::singleton();
177 $info = $gen->getTimeAndDelay( 'lockFile128', 16384, 1048576, 1048576 );
178 $info['offsetCounter'] = $info['offsetCounter'] % 1048576;
179
180 return Wikimedia\base_convert( $gen->getTimestampedID128( $info ), 2, $base );
181 }
182
189 protected function getTimestampedID128( array $info ) {
190 if ( isset( $info['time'] ) ) {
191 $time = $info['time'];
192 $counter = $info['offsetCounter'];
193 $clkSeq = $info['clkSeq'];
194 } else {
195 $time = $info[0];
196 $counter = $info[1];
197 $clkSeq = $info[2];
198 }
199 // Take the 46 LSBs of "milliseconds since epoch"
200 $id_bin = $this->millisecondsSinceEpochBinary( $time );
201 // Add a 20 bit counter resulting in 66 bits total
202 $id_bin .= str_pad( decbin( $counter ), 20, '0', STR_PAD_LEFT );
203 // Add a 14 bit clock sequence number resulting in 80 bits total
204 $id_bin .= str_pad( decbin( $clkSeq ), 14, '0', STR_PAD_LEFT );
205 // Add the 48 bit node ID resulting in 128 bits total
206 $id_bin .= $this->nodeId48;
207 // Convert to a 1-39 digit integer string
208 if ( strlen( $id_bin ) !== 128 ) {
209 throw new RuntimeException( "Detected overflow for millisecond timestamp." );
210 }
211
212 return $id_bin;
213 }
214
222 public static function newUUIDv1() {
223 $gen = self::singleton();
224 // There can be up to 10000 intervals for the same millisecond timestamp.
225 // [0,4999] counter + [0,5000] offset is in [0,9999] for the offset counter.
226 // Add this onto the timestamp to allow making up to 5000 IDs per second.
227 return $gen->getUUIDv1( $gen->getTimeAndDelay( 'lockFileUUID', 16384, 5000, 5001 ) );
228 }
229
237 public static function newRawUUIDv1() {
238 return str_replace( '-', '', self::newUUIDv1() );
239 }
240
245 protected function getUUIDv1( array $info ) {
246 $clkSeq_bin = Wikimedia\base_convert( $info['clkSeq'], 10, 2, 14 );
247 $time_bin = $this->intervalsSinceGregorianBinary( $info['time'], $info['offsetCounter'] );
248 // Take the 32 bits of "time low"
249 $id_bin = substr( $time_bin, 28, 32 );
250 // Add 16 bits of "time mid" resulting in 48 bits total
251 $id_bin .= substr( $time_bin, 12, 16 );
252 // Add 4 bit version resulting in 52 bits total
253 $id_bin .= '0001';
254 // Add 12 bits of "time high" resulting in 64 bits total
255 $id_bin .= substr( $time_bin, 0, 12 );
256 // Add 2 bits of "variant" resulting in 66 bits total
257 $id_bin .= '10';
258 // Add 6 bits of "clock seq high" resulting in 72 bits total
259 $id_bin .= substr( $clkSeq_bin, 0, 6 );
260 // Add 8 bits of "clock seq low" resulting in 80 bits total
261 $id_bin .= substr( $clkSeq_bin, 6, 8 );
262 // Add the 48 bit node ID resulting in 128 bits total
263 $id_bin .= $this->nodeId48;
264 // Convert to a 32 char hex string with dashes
265 if ( strlen( $id_bin ) !== 128 ) {
266 throw new RuntimeException( "Detected overflow for millisecond timestamp." );
267 }
268 $hex = Wikimedia\base_convert( $id_bin, 2, 16, 32 );
269 return sprintf( '%s-%s-%s-%s-%s',
270 // "time_low" (32 bits)
271 substr( $hex, 0, 8 ),
272 // "time_mid" (16 bits)
273 substr( $hex, 8, 4 ),
274 // "time_hi_and_version" (16 bits)
275 substr( $hex, 12, 4 ),
276 // "clk_seq_hi_res" (8 bits) and "clk_seq_low" (8 bits)
277 substr( $hex, 16, 4 ),
278 // "node" (48 bits)
279 substr( $hex, 20, 12 )
280 );
281 }
282
290 public static function newUUIDv4( $flags = 0 ) {
291 $hex = ( $flags & self::QUICK_RAND )
292 ? wfRandomString( 31 )
294
295 return sprintf( '%s-%s-%s-%s-%s',
296 // "time_low" (32 bits)
297 substr( $hex, 0, 8 ),
298 // "time_mid" (16 bits)
299 substr( $hex, 8, 4 ),
300 // "time_hi_and_version" (16 bits)
301 '4' . substr( $hex, 12, 3 ),
302 // "clk_seq_hi_res" (8 bits, variant is binary 10x) and "clk_seq_low" (8 bits)
303 dechex( 0x8 | ( hexdec( $hex[15] ) & 0x3 ) ) . $hex[16] . substr( $hex, 17, 2 ),
304 // "node" (48 bits)
305 substr( $hex, 19, 12 )
306 );
307 }
308
316 public static function newRawUUIDv4( $flags = 0 ) {
317 return str_replace( '-', '', self::newUUIDv4( $flags ) );
318 }
319
332 public static function newSequentialPerNodeID( $bucket, $bits = 48, $flags = 0 ) {
333 return current( self::newSequentialPerNodeIDs( $bucket, $bits, 1, $flags ) );
334 }
335
347 public static function newSequentialPerNodeIDs( $bucket, $bits, $count, $flags = 0 ) {
348 $gen = self::singleton();
349 return $gen->getSequentialPerNodeIDs( $bucket, $bits, $count, $flags );
350 }
351
363 protected function getSequentialPerNodeIDs( $bucket, $bits, $count, $flags ) {
364 if ( $count <= 0 ) {
365 return []; // nothing to do
366 }
367 if ( $bits < 16 || $bits > 48 ) {
368 throw new RuntimeException( "Requested bit size ($bits) is out of range." );
369 }
370
371 $counter = null; // post-increment persistent counter value
372
373 // Use APC/etc if requested, available, and not in CLI mode;
374 // Counter values would not survive across script instances in CLI mode.
375 $cache = null;
376 if ( ( $flags & self::QUICK_VOLATILE ) && !wfIsCLI() ) {
377 $cache = MediaWikiServices::getInstance()->getLocalServerObjectCache();
378 }
379 if ( $cache ) {
380 $counter = $cache->incrWithInit( $bucket, $cache::TTL_INDEFINITE, $count, $count );
381 if ( $counter === false ) {
382 throw new RuntimeException( 'Unable to set value to ' . get_class( $cache ) );
383 }
384 }
385
386 // Note: use of fmod() avoids "division by zero" on 32 bit machines
387 if ( $counter === null ) {
388 $path = wfTempDir() . '/mw-' . __CLASS__ . '-' . rawurlencode( $bucket ) . '-48';
389 // Get the UID lock file handle
390 if ( isset( $this->fileHandles[$path] ) ) {
391 $handle = $this->fileHandles[$path];
392 } else {
393 $handle = fopen( $path, 'cb+' );
394 $this->fileHandles[$path] = $handle ?: null; // cache
395 }
396 // Acquire the UID lock file
397 if ( $handle === false ) {
398 throw new RuntimeException( "Could not open '{$path}'." );
399 }
400 if ( !flock( $handle, LOCK_EX ) ) {
401 fclose( $handle );
402 throw new RuntimeException( "Could not acquire '{$path}'." );
403 }
404 // Fetch the counter value and increment it...
405 rewind( $handle );
406 $counter = floor( trim( fgets( $handle ) ) ) + $count; // fetch as float
407 // Write back the new counter value
408 ftruncate( $handle, 0 );
409 rewind( $handle );
410 fwrite( $handle, fmod( $counter, 2 ** 48 ) ); // warp-around as needed
411 fflush( $handle );
412 // Release the UID lock file
413 flock( $handle, LOCK_UN );
414 }
415
416 $ids = [];
417 $divisor = 2 ** $bits;
418 $currentId = floor( $counter - $count ); // pre-increment counter value
419 for ( $i = 0; $i < $count; ++$i ) {
420 $ids[] = fmod( ++$currentId, $divisor );
421 }
422
423 return $ids;
424 }
425
443 protected function getTimeAndDelay( $lockFile, $clockSeqSize, $counterSize, $offsetSize ) {
444 // Get the UID lock file handle
445 if ( isset( $this->fileHandles[$lockFile] ) ) {
446 $handle = $this->fileHandles[$lockFile];
447 } else {
448 $handle = fopen( $this->$lockFile, 'cb+' );
449 $this->fileHandles[$lockFile] = $handle ?: null; // cache
450 }
451 // Acquire the UID lock file
452 if ( $handle === false ) {
453 throw new RuntimeException( "Could not open '{$this->$lockFile}'." );
454 }
455 if ( !flock( $handle, LOCK_EX ) ) {
456 fclose( $handle );
457 throw new RuntimeException( "Could not acquire '{$this->$lockFile}'." );
458 }
459
460 // The formatters that use this method expect a timestamp with millisecond
461 // precision and a counter upto a certain size. When more IDs than the counter
462 // size are generated during the same timestamp, an exception is thrown as we
463 // cannot increment further, because the formatted ID would not have enough
464 // bits to fit the counter.
465 //
466 // To orchestrate this between independant PHP processes on the same hosts,
467 // we must have a common sense of time so that we only have to maintain
468 // a single counter in a single lock file.
469 //
470 // Given that:
471 // * The system clock can be observed via time(), without milliseconds.
472 // * Some other clock can be observed via microtime(), which also offers
473 // millisecond precision.
474 // * microtime() drifts in-process further and further away from the system
475 // clock the longer the longer the process runs for.
476 // For example, on 2018-10-03 an HHVM 3.18 JobQueue process at WMF,
477 // that ran for 9 min 55 sec, drifted 7 seconds.
478 // The drift is immediate for processes running while the system clock changes.
479 // time() does not have this problem. See https://bugs.php.net/bug.php?id=42659.
480 //
481 // We have two choices:
482 //
483 // 1. Use microtime() with the following caveats:
484 // - The last stored time may be in the future, or our current time
485 // may be in the past, in which case we'll frequently enter the slow
486 // timeWaitUntil() method to try and "sync" the current process with
487 // the previous process. We mustn't block for long though, max 10ms?
488 // - For any drift above 10ms, we pretend that the clock went backwards,
489 // and treat it the same way as after an NTP sync, by incrementing clock
490 // sequence instead. Given this rolls over automatically and silently
491 // and is meant to be rare, this is essentially sacrifices a reasonable
492 // guarantee of uniqueness.
493 // - For long running processes (e.g. longer than a few seconds) the drift
494 // can easily be more than 2 seconds. Because we only have a single lock
495 // file and don't want to keep too many counters and deal with clearing
496 // those, we fatal the user and refuse to make an ID. (T94522)
497 // 2. Use time() and expand the counter by 1000x and use the first digits
498 // as if they are the millisecond fraction of the timestamp.
499 // Known caveats or perf impact: None.
500 //
501 // We choose the latter.
502 $msecCounterSize = $counterSize * 1000;
503
504 rewind( $handle );
505 // Format of lock file contents:
506 // "<clk seq> <sec> <msec counter> <rand offset>"
507 $data = explode( ' ', fgets( $handle ) );
508
509 if ( count( $data ) === 4 ) {
510 // The UID lock file was already initialized
511 $clkSeq = (int)$data[0] % $clockSeqSize;
512 $prevSec = (int)$data[1];
513 // Counter for UIDs with the same timestamp,
514 $msecCounter = 0;
515 $randOffset = (int)$data[3] % $counterSize;
516
517 // If the system clock moved backwards by an NTP sync,
518 // or if the last writer process had its clock drift ahead,
519 // Try to catch up if the gap is small, so that we can keep a single
520 // monotonic logic file.
521 $sec = $this->timeWaitUntil( $prevSec );
522 if ( $sec === false ) {
523 // Gap is too big. Looks like the clock got moved back significantly.
524 // Start a new clock sequence, and re-randomize the extra offset,
525 // which is useful for UIDs that do not include the clock sequence number.
526 $clkSeq = ( $clkSeq + 1 ) % $clockSeqSize;
527 $sec = time();
528 $randOffset = mt_rand( 0, $offsetSize - 1 );
529 trigger_error( "Clock was set back; sequence number incremented." );
530 } elseif ( $sec === $prevSec ) {
531 // Sanity check, only keep remainder if a previous writer wrote
532 // something here that we don't accept.
533 $msecCounter = (int)$data[2] % $msecCounterSize;
534 // Bump the counter if the time has not changed yet
535 if ( ++$msecCounter >= $msecCounterSize ) {
536 // More IDs generated with the same time than counterSize can accomodate
537 flock( $handle, LOCK_UN );
538 throw new RuntimeException( "Counter overflow for timestamp value." );
539 }
540 }
541 } else {
542 // Initialize UID lock file information
543 $clkSeq = mt_rand( 0, $clockSeqSize - 1 );
544 $sec = time();
545 $msecCounter = 0;
546 $randOffset = mt_rand( 0, $offsetSize - 1 );
547 }
548
549 // Update and release the UID lock file
550 ftruncate( $handle, 0 );
551 rewind( $handle );
552 fwrite( $handle, "{$clkSeq} {$sec} {$msecCounter} {$randOffset}" );
553 fflush( $handle );
554 flock( $handle, LOCK_UN );
555
556 // Split msecCounter back into msec and counter
557 $msec = (int)( $msecCounter / 1000 );
558 $counter = $msecCounter % 1000;
559
560 return [
561 'time' => [ $sec, $msec ],
562 'counter' => $counter,
563 'clkSeq' => $clkSeq,
564 'offset' => $randOffset,
565 'offsetCounter' => $counter + $randOffset,
566 ];
567 }
568
576 protected function timeWaitUntil( $time ) {
577 $start = microtime( true );
578 do {
579 $ct = time();
580 // https://secure.php.net/manual/en/language.operators.comparison.php
581 if ( $ct >= $time ) {
582 // current time is higher than or equal to than $time
583 return $ct;
584 }
585 } while ( ( microtime( true ) - $start ) <= 0.010 ); // upto 10ms
586
587 return false;
588 }
589
596 list( $sec, $msec ) = $time;
597 $ts = 1000 * $sec + $msec;
598 if ( $ts > 2 ** 52 ) {
599 throw new RuntimeException( __METHOD__ .
600 ': sorry, this function doesn\'t work after the year 144680' );
601 }
602
603 return substr( Wikimedia\base_convert( $ts, 10, 2, 46 ), -46 );
604 }
605
612 protected function intervalsSinceGregorianBinary( array $time, $delta = 0 ) {
613 list( $sec, $msec ) = $time;
614 $offset = '122192928000000000';
615 if ( PHP_INT_SIZE >= 8 ) { // 64 bit integers
616 $ts = ( 1000 * $sec + $msec ) * 10000 + (int)$offset + $delta;
617 $id_bin = str_pad( decbin( $ts % ( 2 ** 60 ) ), 60, '0', STR_PAD_LEFT );
618 } elseif ( extension_loaded( 'gmp' ) ) {
619 $ts = gmp_add( gmp_mul( (string)$sec, '1000' ), (string)$msec ); // ms
620 $ts = gmp_add( gmp_mul( $ts, '10000' ), $offset ); // 100ns intervals
621 $ts = gmp_add( $ts, (string)$delta );
622 $ts = gmp_mod( $ts, gmp_pow( '2', '60' ) ); // wrap around
623 $id_bin = str_pad( gmp_strval( $ts, 2 ), 60, '0', STR_PAD_LEFT );
624 } elseif ( extension_loaded( 'bcmath' ) ) {
625 $ts = bcadd( bcmul( $sec, 1000 ), $msec ); // ms
626 $ts = bcadd( bcmul( $ts, 10000 ), $offset ); // 100ns intervals
627 $ts = bcadd( $ts, $delta );
628 $ts = bcmod( $ts, bcpow( 2, 60 ) ); // wrap around
629 $id_bin = Wikimedia\base_convert( $ts, 10, 2, 60 );
630 } else {
631 throw new RuntimeException( 'bcmath or gmp extension required for 32 bit machines.' );
632 }
633 return $id_bin;
634 }
635
648 private function deleteCacheFiles() {
649 // T46850
650 foreach ( $this->fileHandles as $path => $handle ) {
651 if ( $handle !== null ) {
652 fclose( $handle );
653 }
654 if ( is_file( $path ) ) {
655 unlink( $path );
656 }
657 unset( $this->fileHandles[$path] );
658 }
659 if ( is_file( $this->nodeIdFile ) ) {
660 unlink( $this->nodeIdFile );
661 }
662 }
663
677 public static function unitTestTearDown() {
678 // T46850
679 $gen = self::singleton();
680 $gen->deleteCacheFiles();
681 }
682
683 function __destruct() {
684 array_map( 'fclose', array_filter( $this->fileHandles ) );
685 }
686}
wfTempDir()
Tries to get the system directory for temporary files.
wfRandomString( $length=32)
Get a random string containing a number of pseudo-random hex characters.
wfShellExec( $cmd, &$retval=null, $environ=[], $limits=[], $options=[])
Execute a shell command, with time and memory limits mirrored from the PHP configuration if supported...
wfIsWindows()
Check if the operating system is Windows.
wfIsCLI()
Check if we are running from the commandline.
$line
Definition cdb.php:59
static generateHex( $chars)
Generate a run of cryptographically random data and return it in hexadecimal string format.
MediaWikiServices is the service locator for the application scope of MediaWiki.
Class for getting statistically unique IDs.
getUUIDv1(array $info)
static newRawUUIDv1()
Return an RFC4122 compliant v1 UUID.
string $nodeIdFile
Local file path.
deleteCacheFiles()
Delete all cache files that have been created.
static newSequentialPerNodeID( $bucket, $bits=48, $flags=0)
Return an ID that is sequential only for this node and bucket.
array $fileHandles
Cached file handles.
millisecondsSinceEpochBinary(array $time)
getTimestampedID88(array $info)
string $lockFile128
Local file path.
const QUICK_VOLATILE
static newSequentialPerNodeIDs( $bucket, $bits, $count, $flags=0)
Return IDs that are sequential only for this node and bucket.
string $nodeId32
Node ID in binary (32 bits)
getTimeAndDelay( $lockFile, $clockSeqSize, $counterSize, $offsetSize)
Get a (time,counter,clock sequence) where (time,counter) is higher than any previous (time,...
string $nodeId48
Node ID in binary (48 bits)
string $lockFile88
Local file path.
static UIDGenerator $instance
static newRawUUIDv4( $flags=0)
Return an RFC4122 compliant v4 UUID.
getSequentialPerNodeIDs( $bucket, $bits, $count, $flags)
Return IDs that are sequential only for this node and bucket.
timeWaitUntil( $time)
Wait till the current timestamp reaches $time and return the current timestamp.
getTimestampedID128(array $info)
intervalsSinceGregorianBinary(array $time, $delta=0)
static newUUIDv4( $flags=0)
Return an RFC4122 compliant v4 UUID.
string $lockFileUUID
Local file path.
static singleton()
static newUUIDv1()
Return an RFC4122 compliant v1 UUID.
static newTimestampedUID128( $base=10)
Get a statistically unique 128-bit unsigned integer ID string.
static unitTestTearDown()
Cleanup resources when tearing down after a unit test.
static newTimestampedUID88( $base=10)
Get a statistically unique 88-bit unsigned integer ID string.
deferred txt A few of the database updates required by various functions here can be deferred until after the result page is displayed to the user For updating the view updating the linked to tables after a etc PHP does not yet have any way to tell the server to actually return and disconnect while still running these but it might have such a feature in the future We handle these by creating a deferred update object and putting those objects on a global list
Definition deferred.txt:11
see documentation in includes Linker php for Linker::makeImageLink & $time
Definition hooks.txt:1841
$cache
Definition mcc.php:33
The wiki should then use memcached to cache various data To use multiple just add more items to the array To increase the weight of a make its entry a array("192.168.0.1:11211", 2))
This program is free software; you can redistribute it and/or modify it under the terms of the GNU Ge...