*/
public function set( $key, $value, $exptime = 0, $flags = 0 ) {
if (
+ is_int( $value ) || // avoid breaking incr()/decr()
( $flags & self::WRITE_ALLOW_SEGMENTS ) != self::WRITE_ALLOW_SEGMENTS ||
is_infinite( $this->segmentationSize )
) {
$segmentHashes[] = $hash;
}
+ $flags &= ~self::WRITE_ALLOW_SEGMENTS; // sanity
$ok = $this->setMulti( $chunksByKey, $exptime, $flags );
if ( $ok ) {
// Only when all segments are stored should the main key be changed
* @since 1.28
*/
public function changeTTL( $key, $exptime = 0, $flags = 0 ) {
+ return $this->doChangeTTL( $key, $exptime, $flags );
+ }
+
+ /**
+ * @param string $key
+ * @param int $exptime
+ * @param int $flags
+ * @return bool
+ */
+ protected function doChangeTTL( $key, $exptime, $flags ) {
$expiry = $this->convertToExpiry( $exptime );
$delete = ( $expiry != 0 && $expiry < $this->getCurrentTime() );
* Get an associative array containing the item for each of the keys that have items.
* @param string[] $keys List of keys
* @param int $flags Bitfield; supports READ_LATEST [optional]
- * @return array
+ * @return array Map of (key => value) for existing keys
*/
public function getMulti( array $keys, $flags = 0 ) {
$valuesBykey = $this->doGetMulti( $keys, $flags );
* Get an associative array containing the item for each of the keys that have items.
* @param string[] $keys List of keys
* @param int $flags Bitfield; supports READ_LATEST [optional]
- * @return array
+ * @return array Map of (key => value) for existing keys
*/
protected function doGetMulti( array $keys, $flags = 0 ) {
$res = [];
* @return bool Success
* @since 1.24
*/
- public function setMulti( array $data, $exptime = 0, $flags = 0 ) {
+ final public function setMulti( array $data, $exptime = 0, $flags = 0 ) {
if ( ( $flags & self::WRITE_ALLOW_SEGMENTS ) === self::WRITE_ALLOW_SEGMENTS ) {
throw new InvalidArgumentException( __METHOD__ . ' got WRITE_ALLOW_SEGMENTS' );
}
+ return $this->doSetMulti( $data, $exptime, $flags );
+ }
+
+ /**
+ * @param mixed[] $data Map of (key => value)
+ * @param int $exptime Either an interval in seconds or a unix timestamp for expiry
+ * @param int $flags Bitfield of BagOStuff::WRITE_* constants
+ * @return bool Success
+ */
+ protected function doSetMulti( array $data, $exptime = 0, $flags = 0 ) {
$res = true;
foreach ( $data as $key => $value ) {
$res = $this->doSet( $key, $value, $exptime, $flags ) && $res;
* @return bool Success
* @since 1.33
*/
- public function deleteMulti( array $keys, $flags = 0 ) {
+ final public function deleteMulti( array $keys, $flags = 0 ) {
+ if ( ( $flags & self::WRITE_ALLOW_SEGMENTS ) === self::WRITE_ALLOW_SEGMENTS ) {
+ throw new InvalidArgumentException( __METHOD__ . ' got WRITE_ALLOW_SEGMENTS' );
+ }
+
+ return $this->doDeleteMulti( $keys, $flags );
+ }
+
+ /**
+ * @param string[] $keys List of keys
+ * @param int $flags Bitfield of BagOStuff::WRITE_* constants
+ * @return bool Success
+ */
+ protected function doDeleteMulti( array $keys, $flags = 0 ) {
$res = true;
foreach ( $keys as $key ) {
$res = $this->doDelete( $key, $flags ) && $res;
return $res;
}
+ /**
+ * Change the expiration of multiple keys that exist
+ *
+ * @see BagOStuff::changeTTL()
+ *
+ * @param string[] $keys List of keys
+ * @param int $exptime TTL or UNIX timestamp
+ * @param int $flags Bitfield of BagOStuff::WRITE_* constants (since 1.33)
+ * @return bool Success
+ * @since 1.34
+ */
+ public function changeTTLMulti( array $keys, $exptime, $flags = 0 ) {
+ $res = true;
+ foreach ( $keys as $key ) {
+ $res = $this->doChangeTTL( $key, $exptime, $flags ) && $res;
+ }
+
+ return $res;
+ }
+
/**
* Increase stored value of $key by $value while preserving its TTL
* @param string $key Key to increase
}
/**
- * Convert an optionally relative time to an absolute time
- * @param int $exptime
+ * Convert an optionally relative timestamp to an absolute time
+ *
+ * The input value will be cast to an integer and interpreted as follows:
+ * - zero: no expiry; return zero (e.g. TTL_INDEFINITE)
+ * - negative: relative TTL; return UNIX timestamp offset by this value
+ * - positive (< 10 years): relative TTL; return UNIX timestamp offset by this value
+ * - positive (>= 10 years): absolute UNIX timestamp; return this value
+ *
+ * @param int $exptime Absolute TTL or 0 for indefinite
* @return int
*/
protected function convertToExpiry( $exptime ) {
- if ( $this->expiryIsRelative( $exptime ) ) {
- return (int)$this->getCurrentTime() + $exptime;
- } else {
- return $exptime;
- }
+ $exptime = (int)$exptime; // sanity
+
+ return $this->expiryIsRelative( $exptime )
+ ? (int)$this->getCurrentTime() + $exptime
+ : $exptime;
}
/**
protected $connFailureErrors = [];
/** @var int */
- const GARBAGE_COLLECT_DELAY_SEC = 1;
+ private static $GARBAGE_COLLECT_DELAY_SEC = 1;
+
+ /** @var string */
+ private static $OP_SET = 'set';
+ /** @var string */
+ private static $OP_ADD = 'add';
+ /** @var string */
+ private static $OP_TOUCH = 'touch';
+ /** @var string */
+ private static $OP_DELETE = 'delete';
/**
* Constructor. Parameters are:
if ( isset( $dataRows[$key] ) ) { // HIT?
$row = $dataRows[$key];
$this->debug( "get: retrieved data; expiry time is " . $row->exptime );
- $db = null;
+ $db = null; // in case of connection failure
try {
$db = $this->getDB( $row->serverIndex );
if ( $this->isExpired( $db, $row->exptime ) ) { // MISS
return $values;
}
- public function setMulti( array $data, $expiry = 0, $flags = 0 ) {
- return $this->insertMulti( $data, $expiry, $flags, true );
+ public function doSetMulti( array $data, $exptime = 0, $flags = 0 ) {
+ return $this->modifyMulti( $data, $exptime, $flags, self::$OP_SET );
}
- private function insertMulti( array $data, $expiry, $flags, $replace ) {
+ /**
+ * @param mixed[]|null[] $data Map of (key => new value or null)
+ * @param int $exptime UNIX timestamp, TTL in seconds, or 0 (no expiration)
+ * @param int $flags Bitfield of BagOStuff::WRITE_* constants
+ * @param string $op Cache operation
+ * @return bool
+ */
+ private function modifyMulti( array $data, $exptime, $flags, $op ) {
$keysByTable = [];
foreach ( $data as $key => $value ) {
list( $serverIndex, $tableName ) = $this->getTableByKey( $key );
$keysByTable[$serverIndex][$tableName][] = $key;
}
+ $exptime = $this->convertToExpiry( $exptime );
+
$result = true;
- $exptime = (int)$expiry;
/** @noinspection PhpUnusedLocalVariableInspection */
$silenceScope = $this->silenceTransactionProfiler();
foreach ( $keysByTable as $serverIndex => $serverKeys ) {
- $db = null;
+ $db = null; // in case of connection failure
try {
$db = $this->getDB( $serverIndex );
- $this->occasionallyGarbageCollect( $db );
+ $this->occasionallyGarbageCollect( $db ); // expire old entries if any
+ $dbExpiry = $exptime ? $db->timestamp( $exptime ) : $this->getMaxDateTime( $db );
} catch ( DBError $e ) {
$this->handleWriteError( $e, $db, $serverIndex );
$result = false;
continue;
}
- if ( $exptime < 0 ) {
- $exptime = 0;
- }
-
- if ( $exptime == 0 ) {
- $encExpiry = $this->getMaxDateTime( $db );
- } else {
- $exptime = $this->convertToExpiry( $exptime );
- $encExpiry = $db->timestamp( $exptime );
- }
foreach ( $serverKeys as $tableName => $tableKeys ) {
- $rows = [];
- foreach ( $tableKeys as $key ) {
- $rows[] = [
- 'keyname' => $key,
- 'value' => $db->encodeBlob( $this->serialize( $data[$key] ) ),
- 'exptime' => $encExpiry,
- ];
- }
-
try {
- if ( $replace ) {
- $db->replace( $tableName, [ 'keyname' ], $rows, __METHOD__ );
- } else {
- $db->insert( $tableName, $rows, __METHOD__, [ 'IGNORE' ] );
- $result = ( $db->affectedRows() > 0 && $result );
- }
+ $result = $this->updateTableKeys(
+ $op,
+ $db,
+ $tableName,
+ $tableKeys,
+ $data,
+ $dbExpiry
+ ) && $result;
} catch ( DBError $e ) {
$this->handleWriteError( $e, $db, $serverIndex );
$result = false;
return $result;
}
- protected function doSet( $key, $value, $exptime = 0, $flags = 0 ) {
- $ok = $this->insertMulti( [ $key => $value ], $exptime, $flags, true );
+ /**
+ * @param string $op
+ * @param IDatabase $db
+ * @param string $table
+ * @param string[] $tableKeys Keys in $data to update
+ * @param mixed[]|null[] $data Map of (key => new value or null)
+ * @param string $dbExpiry DB-encoded expiry
+ * @return bool
+ * @throws DBError
+ * @throws InvalidArgumentException
+ */
+ private function updateTableKeys( $op, $db, $table, $tableKeys, $data, $dbExpiry ) {
+ $success = true;
- return $ok;
+ if ( $op === self::$OP_ADD ) {
+ $rows = [];
+ foreach ( $tableKeys as $key ) {
+ $rows[] = [
+ 'keyname' => $key,
+ 'value' => $db->encodeBlob( $this->serialize( $data[$key] ) ),
+ 'exptime' => $dbExpiry
+ ];
+ }
+ $db->delete(
+ $table,
+ [
+ 'keyname' => $tableKeys,
+ 'exptime <= ' . $db->addQuotes( $db->timestamp() )
+ ],
+ __METHOD__
+ );
+ $db->insert( $table, $rows, __METHOD__, [ 'IGNORE' ] );
+
+ $success = ( $db->affectedRows() == count( $rows ) );
+ } elseif ( $op === self::$OP_SET ) {
+ $rows = [];
+ foreach ( $tableKeys as $key ) {
+ $rows[] = [
+ 'keyname' => $key,
+ 'value' => $db->encodeBlob( $this->serialize( $data[$key] ) ),
+ 'exptime' => $dbExpiry
+ ];
+ }
+ $db->replace( $table, [ 'keyname' ], $rows, __METHOD__ );
+ } elseif ( $op === self::$OP_DELETE ) {
+ $db->delete( $table, [ 'keyname' => $tableKeys ], __METHOD__ );
+ } elseif ( $op === self::$OP_TOUCH ) {
+ $db->update(
+ $table,
+ [ 'exptime' => $dbExpiry ],
+ [
+ 'keyname' => $tableKeys,
+ 'exptime > ' . $db->addQuotes( $db->timestamp() )
+ ],
+ __METHOD__
+ );
+
+ $success = ( $db->affectedRows() == count( $tableKeys ) );
+ } else {
+ throw new InvalidArgumentException( "Invalid operation '$op'" );
+ }
+
+ return $success;
}
- public function add( $key, $value, $exptime = 0, $flags = 0 ) {
- $added = $this->insertMulti( [ $key => $value ], $exptime, $flags, false );
+ protected function doSet( $key, $value, $exptime = 0, $flags = 0 ) {
+ return $this->modifyMulti( [ $key => $value ], $exptime, $flags, self::$OP_SET );
+ }
- return $added;
+ public function add( $key, $value, $exptime = 0, $flags = 0 ) {
+ return $this->modifyMulti( [ $key => $value ], $exptime, $flags, self::$OP_ADD );
}
protected function cas( $casToken, $key, $value, $exptime = 0, $flags = 0 ) {
list( $serverIndex, $tableName ) = $this->getTableByKey( $key );
- $db = null;
+ $exptime = $this->convertToExpiry( $exptime );
+
/** @noinspection PhpUnusedLocalVariableInspection */
$silenceScope = $this->silenceTransactionProfiler();
+ $db = null; // in case of connection failure
try {
$db = $this->getDB( $serverIndex );
- $exptime = intval( $exptime );
-
- if ( $exptime < 0 ) {
- $exptime = 0;
- }
-
- if ( $exptime == 0 ) {
- $encExpiry = $this->getMaxDateTime( $db );
- } else {
- $exptime = $this->convertToExpiry( $exptime );
- $encExpiry = $db->timestamp( $exptime );
- }
// (T26425) use a replace if the db supports it instead of
// delete/insert to avoid clashes with conflicting keynames
$db->update(
[
'keyname' => $key,
'value' => $db->encodeBlob( $this->serialize( $value ) ),
- 'exptime' => $encExpiry
+ 'exptime' => $exptime
+ ? $db->timestamp( $exptime )
+ : $this->getMaxDateTime( $db )
],
[
'keyname' => $key,
- 'value' => $db->encodeBlob( $casToken )
+ 'value' => $db->encodeBlob( $casToken ),
+ 'exptime > ' . $db->addQuotes( $db->timestamp() )
],
__METHOD__
);
return (bool)$db->affectedRows();
}
- public function deleteMulti( array $keys, $flags = 0 ) {
- return $this->purgeMulti( $keys, $flags );
- }
-
- public function purgeMulti( array $keys, $flags = 0 ) {
- $keysByTable = [];
- foreach ( $keys as $key ) {
- list( $serverIndex, $tableName ) = $this->getTableByKey( $key );
- $keysByTable[$serverIndex][$tableName][] = $key;
- }
-
- $result = true;
- /** @noinspection PhpUnusedLocalVariableInspection */
- $silenceScope = $this->silenceTransactionProfiler();
- foreach ( $keysByTable as $serverIndex => $serverKeys ) {
- $db = null;
- try {
- $db = $this->getDB( $serverIndex );
- } catch ( DBError $e ) {
- $this->handleWriteError( $e, $db, $serverIndex );
- $result = false;
- continue;
- }
-
- foreach ( $serverKeys as $tableName => $tableKeys ) {
- try {
- $db->delete( $tableName, [ 'keyname' => $tableKeys ], __METHOD__ );
- } catch ( DBError $e ) {
- $this->handleWriteError( $e, $db, $serverIndex );
- $result = false;
- }
-
- }
- }
-
- if ( ( $flags & self::WRITE_SYNC ) == self::WRITE_SYNC ) {
- $result = $this->waitForReplication() && $result;
- }
-
- return $result;
+ public function doDeleteMulti( array $keys, $flags = 0 ) {
+ return $this->modifyMulti(
+ array_fill_keys( $keys, null ),
+ 0,
+ $flags,
+ self::$OP_DELETE
+ );
}
protected function doDelete( $key, $flags = 0 ) {
- $ok = $this->purgeMulti( [ $key ], $flags );
-
- return $ok;
+ return $this->modifyMulti( [ $key => null ], 0, $flags, self::$OP_DELETE );
}
public function incr( $key, $step = 1 ) {
list( $serverIndex, $tableName ) = $this->getTableByKey( $key );
- $db = null;
+
+ $newCount = false;
/** @noinspection PhpUnusedLocalVariableInspection */
$silenceScope = $this->silenceTransactionProfiler();
+ $db = null; // in case of connection failure
try {
$db = $this->getDB( $serverIndex );
- $step = intval( $step );
- $row = $db->selectRow(
- $tableName,
- [ 'value', 'exptime' ],
- [ 'keyname' => $key ],
- __METHOD__,
- [ 'FOR UPDATE' ]
- );
- if ( $row === false ) {
- // Missing
- return false;
- }
- $db->delete( $tableName, [ 'keyname' => $key ], __METHOD__ );
- if ( $this->isExpired( $db, $row->exptime ) ) {
- // Expired, do not reinsert
- return false;
- }
-
- $oldValue = intval( $this->unserialize( $db->decodeBlob( $row->value ) ) );
- $newValue = $oldValue + $step;
- $db->insert(
+ $encTimestamp = $db->addQuotes( $db->timestamp() );
+ $db->update(
$tableName,
- [
- 'keyname' => $key,
- 'value' => $db->encodeBlob( $this->serialize( $newValue ) ),
- 'exptime' => $row->exptime
- ],
- __METHOD__,
- [ 'IGNORE' ]
+ [ 'value = value + ' . (int)$step ],
+ [ 'keyname' => $key, "exptime > $encTimestamp" ],
+ __METHOD__
);
-
- if ( $db->affectedRows() == 0 ) {
- // Race condition. See T30611
- $newValue = false;
+ if ( $db->affectedRows() > 0 ) {
+ $newValue = $db->selectField(
+ $tableName,
+ 'value',
+ [ 'keyname' => $key, "exptime > $encTimestamp" ],
+ __METHOD__
+ );
+ if ( $this->isInteger( $newValue ) ) {
+ $newCount = (int)$newValue;
+ }
}
} catch ( DBError $e ) {
$this->handleWriteError( $e, $db, $serverIndex );
- return false;
}
- return $newValue;
+ return $newCount;
}
public function merge( $key, callable $callback, $exptime = 0, $attempts = 10, $flags = 0 ) {
return $ok;
}
- public function changeTTL( $key, $exptime = 0, $flags = 0 ) {
- list( $serverIndex, $tableName ) = $this->getTableByKey( $key );
- $db = null;
- /** @noinspection PhpUnusedLocalVariableInspection */
- $silenceScope = $this->silenceTransactionProfiler();
- try {
- $db = $this->getDB( $serverIndex );
- if ( $exptime == 0 ) {
- $timestamp = $this->getMaxDateTime( $db );
- } else {
- $timestamp = $db->timestamp( $this->convertToExpiry( $exptime ) );
- }
- $db->update(
- $tableName,
- [ 'exptime' => $timestamp ],
- [ 'keyname' => $key, 'exptime > ' . $db->addQuotes( $db->timestamp( time() ) ) ],
- __METHOD__
- );
- if ( $db->affectedRows() == 0 ) {
- $exists = (bool)$db->selectField(
- $tableName,
- 1,
- [ 'keyname' => $key, 'exptime' => $timestamp ],
- __METHOD__,
- [ 'FOR UPDATE' ]
- );
-
- return $exists;
- }
- } catch ( DBError $e ) {
- $this->handleWriteError( $e, $db, $serverIndex );
- return false;
- }
+ public function changeTTLMulti( array $keys, $exptime, $flags = 0 ) {
+ return $this->modifyMulti(
+ array_fill_keys( $keys, null ),
+ $exptime,
+ $flags,
+ self::$OP_TOUCH
+ );
+ }
- return true;
+ protected function doChangeTTL( $key, $exptime, $flags ) {
+ return $this->modifyMulti( [ $key => null ], $exptime, $flags, self::$OP_TOUCH );
}
/**
// Only purge on one in every $this->purgePeriod writes
mt_rand( 0, $this->purgePeriod - 1 ) == 0 &&
// Avoid repeating the delete within a few seconds
- ( time() - $this->lastGarbageCollect ) > self::GARBAGE_COLLECT_DELAY_SEC
+ ( time() - $this->lastGarbageCollect ) > self::$GARBAGE_COLLECT_DELAY_SEC
) {
$garbageCollector = function () use ( $db ) {
$this->deleteServerObjectsExpiringBefore( $db, time(), null, $this->purgeLimit );
$keysDeletedCount = 0;
foreach ( $serverIndexes as $numServersDone => $serverIndex ) {
- $db = null;
+ $db = null; // in case of connection failure
try {
$db = $this->getDB( $serverIndex );
$this->deleteServerObjectsExpiringBefore(
/** @noinspection PhpUnusedLocalVariableInspection */
$silenceScope = $this->silenceTransactionProfiler();
for ( $serverIndex = 0; $serverIndex < $this->numServers; $serverIndex++ ) {
- $db = null;
+ $db = null; // in case of connection failure
try {
$db = $this->getDB( $serverIndex );
for ( $i = 0; $i < $this->shards; $i++ ) {
list( $serverIndex ) = $this->getTableByKey( $key );
- $db = null;
+ $db = null; // in case of connection failure
try {
$db = $this->getDB( $serverIndex );
$ok = $db->lock( $key, __METHOD__, $timeout );
list( $serverIndex ) = $this->getTableByKey( $key );
- $db = null;
+ $db = null; // in case of connection failure
try {
$db = $this->getDB( $serverIndex );
$ok = $db->unlock( $key, __METHOD__ );
$this->handleWriteError( $e, $db, $serverIndex );
$ok = false;
}
- } else {
- $ok = false;
+
+ return $ok;
}
- return $ok;
+ return true;
}
/**
* in storage requirements.
*
* @param mixed $data
- * @return string
+ * @return string|int
*/
protected function serialize( $data ) {
- $serial = serialize( $data );
+ if ( is_int( $data ) ) {
+ return $data;
+ }
+ $serial = serialize( $data );
if ( function_exists( 'gzdeflate' ) ) {
- return gzdeflate( $serial );
- } else {
- return $serial;
+ $serial = gzdeflate( $serial );
}
+
+ return $serial;
}
/**
* @return mixed
*/
protected function unserialize( $serial ) {
+ if ( $this->isInteger( $serial ) ) {
+ return (int)$serial;
+ }
+
if ( function_exists( 'gzinflate' ) ) {
Wikimedia\suppressWarnings();
$decomp = gzinflate( $serial );
}
}
- $ret = unserialize( $serial );
-
- return $ret;
+ return unserialize( $serial );
}
/**