$link = wfMessage( 'rev-deleted-user' )->escaped();
}
if ( $rev->isDeleted( Revision::DELETED_USER ) ) {
- return ' <span class="history-deleted">' . $link . '</span>';
+ return ' <span class="history-deleted mw-userlink">' . $link . '</span>';
}
return $link;
}
$block = " <span class=\"comment\">" . wfMessage( 'rev-deleted-comment' )->escaped() . "</span>";
}
if ( $rev->isDeleted( Revision::DELETED_COMMENT ) ) {
- return " <span class=\"history-deleted\">$block</span>";
+ return " <span class=\"history-deleted comment\">$block</span>";
}
return $block;
}
$link = htmlspecialchars( $date );
}
if ( $rev->isDeleted( Revision::DELETED_TEXT ) ) {
- $link = "<span class=\"history-deleted\">$link</span>";
+ $link = "<span class=\"history-deleted mw-changeslist-date\">$link</span>";
}
return $link;
const MAX_CACHE_TTL = 300; // 5 minutes
const MAX_SIGNATURE_TTL = 60;
+ const MAX_CACHE_RECENT = 2;
+
public function execute() {
$user = $this->getUser();
$params = $this->extractRequestParams();
);
}
+ if ( $ok ) {
+ // These blobs can waste slots in low cardinality memcached slabs
+ self::pruneExcessStashedEntries( $cache, $user, $key );
+ }
+
return $ok ? true : 'store_error';
}
+ /**
+ * @param BagOStuff $cache
+ * @param User $user
+ * @param string $newKey
+ */
+ private static function pruneExcessStashedEntries( BagOStuff $cache, User $user, $newKey ) {
+ $key = $cache->makeKey( 'stash-edit-recent', $user->getId() );
+
+ $keyList = $cache->get( $key ) ?: [];
+ if ( count( $keyList ) >= self::MAX_CACHE_RECENT ) {
+ $oldestKey = array_shift( $keyList );
+ $cache->delete( $oldestKey );
+ }
+
+ $keyList[] = $newKey;
+ $cache->set( $key, $keyList, 2 * self::MAX_CACHE_TTL );
+ }
+
public function getAllowedParams() {
return [
'title' => [
*/
abstract public function delete( $key, $flags = 0 );
+ /**
+ * Insert an item if it does not already exist
+ *
+ * @param string $key
+ * @param mixed $value
+ * @param int $exptime
+ * @param int $flags Bitfield of BagOStuff::WRITE_* constants (since 1.33)
+ * @return bool Success
+ */
+ abstract public function add( $key, $value, $exptime = 0, $flags = 0 );
+
/**
* Merge changes into the existing cache value (possibly creating a new one)
*
return $res;
}
- /**
- * Insertion
- * @param string $key
- * @param mixed $value
- * @param int $exptime
- * @param int $flags Bitfield of BagOStuff::WRITE_* constants (since 1.33)
- * @return bool Success
- */
- abstract public function add( $key, $value, $exptime = 0, $flags = 0 );
-
/**
* Increase stored value of $key by $value while preserving its TTL
* @param string $key Key to increase
}
}
+ /**
+ * @param int $exptime
+ * @return bool
+ */
+ protected function expiryIsRelative( $exptime ) {
+ return ( $exptime != 0 && $exptime < ( 10 * self::TTL_YEAR ) );
+ }
+
/**
* Convert an optionally relative time to an absolute time
* @param int $exptime
* @return int
*/
- protected function convertExpiry( $exptime ) {
- if ( $exptime != 0 && $exptime < ( 10 * self::TTL_YEAR ) ) {
+ protected function convertToExpiry( $exptime ) {
+ if ( $this->expiryIsRelative( $exptime ) ) {
return (int)$this->getCurrentTime() + $exptime;
} else {
return $exptime;
unset( $this->bag[$key] );
$this->bag[$key] = [
self::KEY_VAL => $value,
- self::KEY_EXP => $this->convertExpiry( $exptime ),
+ self::KEY_EXP => $this->convertToExpiry( $exptime ),
self::KEY_CAS => $this->token . ':' . ++self::$casCounter
];
*/
/**
- * Generic base class for storage interfaces.
+ * Generic interface for lightweight expiring object stores.
*
* Provides convenient TTL constants.
*
const TTL_INDEFINITE = 0;
- // Attribute and QoS constants; higher QOS values with the same prefix rank higher...
- // Medium attributes constants related to emulation or media type
+ // Emulation/fallback medium attribute (e.g. SQLBagOStuff)
const ATTR_EMULATION = 1;
+ // Quality of service constants for ATTR_EMULATION (higher means faster)
const QOS_EMULATION_SQL = 1;
- // Medium attributes constants related to replica consistency
- const ATTR_SYNCWRITES = 2; // SYNC_WRITES flag support
+
+ // Replica synchronization/consistency attribute of medium when SYNC_WRITES is used
+ const ATTR_SYNCWRITES = 2;
+ // Quality of service constants for ATTR_SYNCWRITES (higher means more consistent)
const QOS_SYNCWRITES_NONE = 1; // replication only supports eventual consistency or less
const QOS_SYNCWRITES_BE = 2; // best effort synchronous with limited retries
const QOS_SYNCWRITES_QC = 3; // write quorum applied directly to state machines where R+W > N
const QOS_SYNCWRITES_SS = 4; // strict-serializable, nodes refuse reads if possible stale
+
// Generic "unknown" value that is useful for comparisons (e.g. always good enough)
const QOS_UNKNOWN = INF;
throw new DBUnexpectedError( $this->conn, 'Cannot close shared connection.' );
}
- public function query( $sql, $fname = __METHOD__, $tempIgnore = false ) {
+ public function query( $sql, $fname = __METHOD__, $flags = 0 ) {
return $this->__call( __FUNCTION__, func_get_args() );
}
/** @var int No transaction is active */
const STATUS_TRX_NONE = 3;
+ /** @var int Writes to this temporary table do not affect lastDoneWrites() */
+ const TEMP_NORMAL = 1;
+ /** @var int Writes to this temporary table effect lastDoneWrites() */
+ const TEMP_PSEUDO_PERMANENT = 2;
+
/**
* @note exceptions for missing libraries/drivers should be thrown in initConnection()
* @param array $params Parameters passed from Database::factory()
/**
* @param string $sql A SQL query
- * @return bool Whether $sql is SQL for TEMPORARY table operation
+ * @param bool $pseudoPermanent Treat any table from CREATE TEMPORARY as pseudo-permanent
+ * @return int|null A self::TEMP_* constant for temp table operations or null otherwise
*/
- protected function registerTempTableOperation( $sql ) {
+ protected function registerTempTableWrite( $sql, $pseudoPermanent ) {
+ static $qt = '[`"\']?(\w+)[`"\']?'; // quoted table
+
if ( preg_match(
- '/^CREATE\s+TEMPORARY\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?[`"\']?(\w+)[`"\']?/i',
+ '/^CREATE\s+TEMPORARY\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?' . $qt . '/i',
$sql,
$matches
) ) {
- $this->sessionTempTables[$matches[1]] = 1;
+ $type = $pseudoPermanent ? self::TEMP_PSEUDO_PERMANENT : self::TEMP_NORMAL;
+ $this->sessionTempTables[$matches[1]] = $type;
- return true;
+ return $type;
} elseif ( preg_match(
- '/^DROP\s+(?:TEMPORARY\s+)?TABLE\s+(?:IF\s+EXISTS\s+)?[`"\']?(\w+)[`"\']?/i',
+ '/^DROP\s+(?:TEMPORARY\s+)?TABLE\s+(?:IF\s+EXISTS\s+)?' . $qt . '/i',
$sql,
$matches
) ) {
- $isTemp = isset( $this->sessionTempTables[$matches[1]] );
+ $type = $this->sessionTempTables[$matches[1]] ?? null;
unset( $this->sessionTempTables[$matches[1]] );
- return $isTemp;
+ return $type;
} elseif ( preg_match(
- '/^TRUNCATE\s+(?:TEMPORARY\s+)?TABLE\s+(?:IF\s+EXISTS\s+)?[`"\']?(\w+)[`"\']?/i',
+ '/^TRUNCATE\s+(?:TEMPORARY\s+)?TABLE\s+(?:IF\s+EXISTS\s+)?' . $qt . '/i',
$sql,
$matches
) ) {
- return isset( $this->sessionTempTables[$matches[1]] );
+ return $this->sessionTempTables[$matches[1]] ?? null;
} elseif ( preg_match(
- '/^(?:INSERT\s+(?:\w+\s+)?INTO|UPDATE|DELETE\s+FROM)\s+[`"\']?(\w+)[`"\']?/i',
+ '/^(?:(?:INSERT|REPLACE)\s+(?:\w+\s+)?INTO|UPDATE|DELETE\s+FROM)\s+' . $qt . '/i',
$sql,
$matches
) ) {
- return isset( $this->sessionTempTables[$matches[1]] );
+ return $this->sessionTempTables[$matches[1]] ?? null;
}
- return false;
+ return null;
}
- public function query( $sql, $fname = __METHOD__, $tempIgnore = false ) {
+ public function query( $sql, $fname = __METHOD__, $flags = 0 ) {
$this->assertTransactionStatus( $sql, $fname );
$this->assertHasConnectionHandle();
+ $flags = (int)$flags; // b/c; this field used to be a bool
+ $ignoreErrors = $this->hasFlags( $flags, self::QUERY_SILENCE_ERRORS );
+ $pseudoPermanent = $this->hasFlags( $flags, self::QUERY_PSEUDO_PERMANENT );
+
$priorTransaction = $this->trxLevel;
$priorWritesPending = $this->writesOrCallbacksPending();
$this->lastQuery = $sql;
# In theory, non-persistent writes are allowed in read-only mode, but due to things
# like https://bugs.mysql.com/bug.php?id=33669 that might not work anyway...
$this->assertIsWritableMaster();
- # Avoid treating temporary table operations as meaningful "writes"
- $isEffectiveWrite = !$this->registerTempTableOperation( $sql );
+ # Do not treat temporary table writes as "meaningful writes" that need committing.
+ # Profile them as reads. Integration tests can override this behavior via $flags.
+ $tableType = $this->registerTempTableWrite( $sql, $pseudoPermanent );
+ $isEffectiveWrite = ( $tableType !== self::TEMP_NORMAL );
} else {
$isEffectiveWrite = false;
}
$this->trxStatus = self::STATUS_TRX_ERROR;
$this->trxStatusCause =
$this->getQueryExceptionAndLog( $lastError, $lastErrno, $sql, $fname );
- $tempIgnore = false; // cannot recover
+ $ignoreErrors = false; // cannot recover
$this->trxStatusIgnoredCause = null;
}
}
- $this->reportQueryError( $lastError, $lastErrno, $sql, $fname, $tempIgnore );
+ $this->reportQueryError( $lastError, $lastErrno, $sql, $fname, $ignoreErrors );
}
return $this->resultObject( $ret );
/**
* Report a query error. Log the error, and if neither the object ignore
- * flag nor the $tempIgnore flag is set, throw a DBQueryError.
+ * flag nor the $ignoreErrors flag is set, throw a DBQueryError.
*
* @param string $error
* @param int $errno
* @param string $sql
* @param string $fname
- * @param bool $tempIgnore
+ * @param bool $ignoreErrors
* @throws DBQueryError
*/
- public function reportQueryError( $error, $errno, $sql, $fname, $tempIgnore = false ) {
- if ( $tempIgnore ) {
+ public function reportQueryError( $error, $errno, $sql, $fname, $ignoreErrors = false ) {
+ if ( $ignoreErrors ) {
$this->queryLogger->debug( "SQL ERROR (ignored): $error\n" );
} else {
$exception = $this->getQueryExceptionAndLog( $error, $errno, $sql, $fname );
$this->indexAliases = $aliases;
}
+ /**
+ * @param int $field
+ * @param int $flags
+ * @return bool
+ */
+ protected function hasFlags( $field, $flags ) {
+ return ( ( $field & $flags ) === $flags );
+ }
+
/**
* Get the underlying binding connection handle
*
$oldName = $this->addIdentifierQuotes( $oldName );
$query = "CREATE $tmp TABLE $newName (LIKE $oldName)";
- return $this->query( $query, $fname );
+ return $this->query( $query, $fname, $this::QUERY_PSEUDO_PERMANENT );
}
/**
$temporary = $temporary ? 'TEMPORARY' : '';
- $ret = $this->query( "CREATE $temporary TABLE $newNameE " .
- "(LIKE $oldNameE INCLUDING DEFAULTS INCLUDING INDEXES)", $fname );
+ $ret = $this->query(
+ "CREATE $temporary TABLE $newNameE " .
+ "(LIKE $oldNameE INCLUDING DEFAULTS INCLUDING INDEXES)",
+ $fname,
+ $this::QUERY_PSEUDO_PERMANENT
+ );
if ( !$ret ) {
return $ret;
}
$fieldE = $this->addIdentifierQuotes( $field );
$newSeqE = $this->addIdentifierQuotes( $newSeq );
$newSeqQ = $this->addQuotes( $newSeq );
- $this->query( "CREATE $temporary SEQUENCE $newSeqE OWNED BY $newNameE.$fieldE", $fname );
+ $this->query(
+ "CREATE $temporary SEQUENCE $newSeqE OWNED BY $newNameE.$fieldE",
+ $fname
+ );
$this->query(
"ALTER TABLE $newNameE ALTER COLUMN $fieldE SET DEFAULT nextval({$newSeqQ}::regclass)",
$fname
}
}
- $res = $this->query( $sql, $fname );
+ $res = $this->query( $sql, $fname, self::QUERY_PSEUDO_PERMANENT );
// Take over indexes
$indexList = $this->query( 'PRAGMA INDEX_LIST(' . $this->addQuotes( $oldName ) . ')' );
/** @var int Enable compression in connection protocol */
const DBO_COMPRESS = 512;
+ /** @var int Ignore query errors and return false when they happen */
+ const QUERY_SILENCE_ERRORS = 1; // b/c for 1.32 query() argument; note that (int)true = 1
+ /**
+ * @var int Treat the TEMPORARY table from the given CREATE query as if it is
+ * permanent as far as write tracking is concerned. This is useful for testing.
+ */
+ const QUERY_PSEUDO_PERMANENT = 2;
+
/**
* A string describing the current software version, and possibly
* other details in a user-friendly way. Will be listed on Special:Version, etc.
* @param string $sql SQL query
* @param string $fname Name of the calling function, for profiling/SHOW PROCESSLIST
* comment (you can use __METHOD__ or add some extra info)
- * @param bool $tempIgnore Whether to avoid throwing an exception on errors...
- * maybe best to catch the exception instead?
+ * @param int $flags Bitfield of IDatabase::QUERY_* constants. Note that suppression
+ * of errors is best handled by try/catch rather than using one of these flags.
* @return bool|IResultWrapper True for a successful write query, IResultWrapper object
- * for a successful read query, or false on failure if $tempIgnore set
+ * for a successful read query, or false on failure if QUERY_SILENCE_ERRORS is set.
* @throws DBError
*/
- public function query( $sql, $fname = __METHOD__, $tempIgnore = false );
+ public function query( $sql, $fname = __METHOD__, $flags = 0 );
/**
* Free a result object returned by query() or select(). It's usually not
if ( $exptime == 0 ) {
$encExpiry = $this->getMaxDateTime( $db );
} else {
- $exptime = $this->convertExpiry( $exptime );
+ $exptime = $this->convertToExpiry( $exptime );
$encExpiry = $db->timestamp( $exptime );
}
foreach ( $serverKeys as $tableName => $tableKeys ) {
if ( $exptime == 0 ) {
$encExpiry = $this->getMaxDateTime( $db );
} else {
- $exptime = $this->convertExpiry( $exptime );
+ $exptime = $this->convertToExpiry( $exptime );
$encExpiry = $db->timestamp( $exptime );
}
// (T26425) use a replace if the db supports it instead of
$db = $this->getDB( $serverIndex );
$db->update(
$tableName,
- [ 'exptime' => $db->timestamp( $this->convertExpiry( $expiry ) ) ],
+ [ 'exptime' => $db->timestamp( $this->convertToExpiry( $expiry ) ) ],
[ 'keyname' => $key, 'exptime > ' . $db->addQuotes( $db->timestamp( time() ) ) ],
__METHOD__
);
],
'jquery.spinner' => [
'scripts' => 'resources/src/jquery.spinner/spinner.js',
- 'styles' => 'resources/src/jquery.spinner/spinner.css',
+ 'styles' => 'resources/src/jquery.spinner/spinner.less',
'targets' => [ 'desktop', 'mobile' ],
],
'jquery.jStorage' => [
'styles' => 'resources/src/mediawiki.action/mediawiki.action.history.css',
],
'mediawiki.action.history.styles' => [
- 'styles' => 'resources/src/mediawiki.action/mediawiki.action.history.styles.css',
+ 'skinStyles' => [
+ 'default' => 'resources/src/mediawiki.action/mediawiki.action.history.styles.css',
+ ],
],
'mediawiki.action.view.dblClickEdit' => [
'scripts' => 'resources/src/mediawiki.action/mediawiki.action.view.dblClickEdit.js',
'parentheses-end',
'pipe-separator'
],
- 'styles' => [
- 'resources/src/mediawiki.interface.helpers.styles.less',
+ 'skinStyles' => [
+ 'default' => 'resources/src/mediawiki.interface.helpers.styles.less',
],
'targets' => [
'desktop', 'mobile'
+++ /dev/null
-.mw-spinner {
- background-color: transparent;
- background-position: center center;
- background-repeat: no-repeat;
-}
-
-.mw-spinner-small {
- /* @embed */
- background-image: url( images/spinner.gif );
- height: 20px;
- width: 20px;
- /* Avoid issues with .mw-spinner-block when floated without width. */
- min-width: 20px;
-}
-
-.mw-spinner-large {
- /* @embed */
- background-image: url( images/spinner-large.gif );
- height: 32px;
- width: 32px;
- /* Avoid issues with .mw-spinner-block when floated without width. */
- min-width: 32px;
-}
-
-.mw-spinner-block {
- display: block;
- /* This overrides width from .mw-spinner-large / .mw-spinner-small,
- * This is where the min-width kicks in.
- */
- width: 100%;
-}
-
-.mw-spinner-inline {
- display: inline-block;
- vertical-align: middle;
-}
* @return {jQuery}
*/
createSpinner: function ( opts ) {
- var $spinner;
+ var i, $spinner, $container;
if ( typeof opts === 'string' ) {
opts = {
$spinner.attr( 'id', 'mw-spinner-' + opts.id );
}
- $spinner.addClass( opts.size === 'large' ? 'mw-spinner-large' : 'mw-spinner-small' );
- $spinner.addClass( opts.type === 'block' ? 'mw-spinner-block' : 'mw-spinner-inline' );
+ $spinner
+ .addClass( opts.size === 'large' ? 'mw-spinner-large' : 'mw-spinner-small' )
+ .addClass( opts.type === 'block' ? 'mw-spinner-block' : 'mw-spinner-inline' );
+
+ $container = $( '<div>' ).addClass( 'mw-spinner-container' ).appendTo( $spinner );
+ for ( i = 0; i < 12; i++ ) {
+ $container.append( $( '<div>' ) );
+ }
return $spinner;
},
--- /dev/null
+.mw-spinner {
+ position: relative;
+
+ > .mw-spinner-container {
+ transform-origin: 0 0;
+ }
+}
+
+@mw-spinner-small-size: 20px;
+@mw-spinner-large-size: 32px;
+
+.mw-spinner-small {
+ width: @mw-spinner-small-size;
+ height: @mw-spinner-small-size;
+
+ > .mw-spinner-container {
+ transform: scale( unit( @mw-spinner-small-size / 64 ) );
+ }
+}
+
+.mw-spinner-large {
+ width: @mw-spinner-large-size;
+ height: @mw-spinner-large-size;
+
+ > .mw-spinner-container {
+ transform: scale( unit( @mw-spinner-large-size / 64 ) );
+ }
+}
+
+.mw-spinner-block {
+ display: block;
+ width: 100%;
+ text-align: center;
+
+ > .mw-spinner-container {
+ display: inline-block;
+ vertical-align: top;
+ }
+
+ &.mw-spinner-small > .mw-spinner-container {
+ min-width: @mw-spinner-small-size;
+ }
+
+ &.mw-spinner-large > .mw-spinner-container {
+ min-width: @mw-spinner-large-size;
+ }
+}
+
+.mw-spinner-inline {
+ display: inline-block;
+ vertical-align: middle;
+}
+
+/**
+ * CSS loading spinner adapted from loadingio, CC0
+ * https://github.com/loadingio/css-spinner/
+ */
+.mw-spinner-container > div {
+ transform-origin: 32px 32px;
+ animation: mw-spinner 1.2s linear infinite;
+}
+
+.mw-spinner-container > div:after {
+ content: ' ';
+ display: block;
+ position: absolute;
+ top: 3px;
+ left: 29px;
+ width: 5px;
+ height: 14px;
+ border-radius: 20%;
+ background: #000;
+}
+
+.mw-spinner-container > div:nth-child( 1 ) {
+ transform: rotate( 0deg );
+ animation-delay: -1.1s;
+}
+
+.mw-spinner-container > div:nth-child( 2 ) {
+ transform: rotate( 30deg );
+ animation-delay: -1s;
+}
+
+.mw-spinner-container > div:nth-child( 3 ) {
+ transform: rotate( 60deg );
+ animation-delay: -0.9s;
+}
+
+.mw-spinner-container > div:nth-child( 4 ) {
+ transform: rotate( 90deg );
+ animation-delay: -0.8s;
+}
+
+.mw-spinner-container > div:nth-child( 5 ) {
+ transform: rotate( 120deg );
+ animation-delay: -0.7s;
+}
+
+.mw-spinner-container > div:nth-child( 6 ) {
+ transform: rotate( 150deg );
+ animation-delay: -0.6s;
+}
+
+.mw-spinner-container > div:nth-child( 7 ) {
+ transform: rotate( 180deg );
+ animation-delay: -0.5s;
+}
+
+.mw-spinner-container > div:nth-child( 8 ) {
+ transform: rotate( 210deg );
+ animation-delay: -0.4s;
+}
+
+.mw-spinner-container > div:nth-child( 9 ) {
+ transform: rotate( 240deg );
+ animation-delay: -0.3s;
+}
+
+.mw-spinner-container > div:nth-child( 10 ) {
+ transform: rotate( 270deg );
+ animation-delay: -0.2s;
+}
+
+.mw-spinner-container > div:nth-child( 11 ) {
+ transform: rotate( 300deg );
+ animation-delay: -0.1s;
+}
+
+.mw-spinner-container > div:nth-child( 12 ) {
+ transform: rotate( 330deg );
+ animation-delay: 0s;
+}
+
+@keyframes mw-spinner {
+ 0% {
+ opacity: 1;
+ }
+
+ 100% {
+ opacity: 0;
+ }
+}
return Database::factory( 'SqliteMock', $p );
}
- function query( $sql, $fname = '', $tempIgnore = false ) {
+ function query( $sql, $fname = '', $flags = 0 ) {
return true;
}
return $s;
}
- public function query( $sql, $fname = '', $tempIgnore = false ) {
+ public function query( $sql, $fname = '', $flags = 0 ) {
$this->checkFunctionName( $fname );
- return parent::query( $sql, $fname, $tempIgnore );
+ return parent::query( $sql, $fname, $flags );
}
public function tableExists( $table, $fname = __METHOD__ ) {
}
/**
- * @covers Wikimedia\Rdbms\Database::registerTempTableOperation
+ * @covers Wikimedia\Rdbms\Database::registerTempTableWrite
*/
public function testSessionTempTables() {
$temp1 = $this->database->tableName( 'tmp_table_1' );