http://www.mediawiki.org/wiki/Special:Code/MediaWiki/96722
Revision: 96722
Author: catrope
Date: 2011-09-10 15:26:11 +0000 (Sat, 10 Sep 2011)
Log Message:
-----------
Epic followup to r94404, r94509: change the ArticleFeedback schema such that
locking is no longer necessary. This should fix the counter drift problems,
too. Thanks to Tim for pointing me in this direction.
* Change the schema of article_feedback to no longer have a primary key that
ensures one rating per user per revision, but to have a primary key on the
auto-incremented field aa_id instead
** This allows multiple ratings per user per revision in the article_feedback
table, which makes the counter drift fix possible
** This schema change is so radical that the patch has to create another table
with the new schema, copy the data over, and swap the tables
* Changed ApiArticleFeedback::insertUserRatings() to insert all the ratings
rows at once, using one query. This ensures they get consecutive aa_id values
* Moved the SELECT in ApiArticleFeedback after the insertUserRatings() call,
and added an "aa_id < $id" clause so it always looks at the rating set right
before the one that was just inserted, even if another thread has already
inserted another rating set in the meantime. This ensures that each rating
delta is handled by exactly one thread, which should fix counter drift.
* Removed the useless and broken behavior of decrementing values in
article_feedback_revisions. It was implemented inconsistently and never worked
properly for that reason. This makes insertRevisionRating() much simpler
* Due to the insertRevisionRating() change. $lastRevision is no longer needed.
To properly update the revision delta, though, we still need to look at whether
the previous rating's revid matches this one's, and only take that rating into
account if it does. The previous logic was probably broken and, now that I
think about it, was probably the cause of all the weird behavior, but it's too
late to go back now. I'll patch 1.17wmf1 for just this problem later, see if
that helps
* Take this opportunity to redo the indexes on article_feedback so the queries
run on it are actually indexed properly. Also drop useless aa_user_id
conditions from queries.
Modified Paths:
--------------
trunk/extensions/ArticleFeedback/ArticleFeedback.hooks.php
trunk/extensions/ArticleFeedback/api/ApiArticleFeedback.php
trunk/extensions/ArticleFeedback/api/ApiQueryArticleFeedback.php
trunk/extensions/ArticleFeedback/sql/ArticleFeedback.sql
Added Paths:
-----------
trunk/extensions/ArticleFeedback/sql/RecreatePK.sql
Modified: trunk/extensions/ArticleFeedback/ArticleFeedback.hooks.php
===================================================================
--- trunk/extensions/ArticleFeedback/ArticleFeedback.hooks.php 2011-09-10
13:04:07 UTC (rev 96721)
+++ trunk/extensions/ArticleFeedback/ArticleFeedback.hooks.php 2011-09-10
15:26:11 UTC (rev 96722)
@@ -230,6 +230,15 @@
$dir .
'/sql/AddArticleFeedbackTimestampIndex.sql',
true
) );
+
+ // This change recreates the PK on a new field. Check
for that new field's existence
+ $updater->addExtensionUpdate( array(
+ 'addField',
+ 'article_feedback',
+ 'aa_id',
+ $dir . '/sql/RecreatePK.sql',
+ true
+ ) );
}
return true;
}
Modified: trunk/extensions/ArticleFeedback/api/ApiArticleFeedback.php
===================================================================
--- trunk/extensions/ArticleFeedback/api/ApiArticleFeedback.php 2011-09-10
13:04:07 UTC (rev 96721)
+++ trunk/extensions/ArticleFeedback/api/ApiArticleFeedback.php 2011-09-10
15:26:11 UTC (rev 96722)
@@ -38,57 +38,60 @@
}
$dbw = wfGetDB( DB_MASTER );
-
- // Query the latest ratings by this user for this page,
+
+ $pageId = $params['pageid'];
+ $revisionId = $params['revid'];
+
+ // Build array( rating ID => rating value )
+ $ratings = array();
+ foreach ( $wgArticleFeedbackRatings as $ratingID ) {
+ $ratings[$ratingID] = intval( $params["r{$ratingID}"] );
+ }
+ // Insert the new ratings
+ $id = $this->insertUserRatings( $pageId, $revisionId, $wgUser,
$token, $ratings, $params['bucket'] );
+
+ // Query the previous ratings by this user for this page,
// possibly for an older revision
- // Select from the master to prevent replag-induced bugs
+ // Use aa_id < $id to make sure we get the one before ours even
if other ratings were inserted after
+ // insertUserRatings() but before selectRow(); this prevents
race conditions from messing things up.
$res = $dbw->select(
'article_feedback',
array( 'aa_rating_id', 'aa_rating_value', 'aa_revision'
),
array(
- 'aa_user_id' => $wgUser->getId(),
'aa_user_text' => $wgUser->getName(),
'aa_page_id' => $params['pageid'],
'aa_rating_id' => $wgArticleFeedbackRatings,
'aa_user_anon_token' => $token,
+ 'aa_id < ' . intval( $id )
),
__METHOD__,
array(
- 'ORDER BY' => 'aa_revision DESC',
+ 'ORDER BY' => 'aa_id DESC',
'LIMIT' => count( $wgArticleFeedbackRatings ),
)
);
-
$lastRatings = array();
-
foreach ( $res as $row ) {
$lastRatings[$row->aa_rating_id]['value'] =
$row->aa_rating_value;
$lastRatings[$row->aa_rating_id]['revision'] =
$row->aa_revision;
}
-
- $pageId = $params['pageid'];
- $revisionId = $params['revid'];
-
- foreach( $wgArticleFeedbackRatings as $rating ) {
- $lastRating = false;
- $lastRevision = false;
+
+ foreach ( $wgArticleFeedbackRatings as $rating ) {
+ $lastPageRating = false;
+ $lastRevRating = false;
if ( isset( $lastRatings[$rating] ) ) {
- $lastRating = intval(
$lastRatings[$rating]['value'] );
- $lastRevision = intval(
$lastRatings[$rating]['revision'] );
+ $lastPageRating = intval(
$lastRatings[$rating]['value'] );
+ if ( intval( $lastRatings[$rating]['revision']
) == $revisionId ) {
+ $lastRevRating = $lastPageRating;
+ }
}
-
- $thisRating = false;
- if ( isset( $params["r{$rating}"] ) ) {
- $thisRating = intval( $params["r{$rating}"] );
- }
-
- $this->insertRevisionRating( $pageId, $revisionId,
$lastRevision, $rating, ( $thisRating - $lastRating ),
- $thisRating, $lastRating
- );
+ $thisRating = intval( $params["r{$rating}"] );
- $this->insertPageRating( $pageId, $rating, (
$thisRating - $lastRating ), $thisRating, $lastRating );
-
- $this->insertUserRatings( $pageId, $revisionId,
$wgUser, $token, $rating, $thisRating, $params['bucket'] );
+ // Update counter tables
+ $this->insertRevisionRating( $pageId, $revisionId,
$rating, $thisRating - $lastRevRating,
+ $thisRating, $lastRevRating
+ );
+ $this->insertPageRating( $pageId, $rating, $thisRating
- $lastPageRating, $thisRating, $lastPageRating );
}
$this->insertProperties( $revisionId, $wgUser, $token, $params
);
@@ -157,13 +160,12 @@
*
* @param $pageId Integer: Page Id
* @param $revisionId Integer: Revision Id
- * @param $lastRevision Integer: Revision Id of last rating
* @param $ratingId Integer: Rating Id
* @param $updateAddition Integer: Difference between user's last
rating (if applicable)
* @param $thisRating Integer|Boolean: Value of the Rating
* @param $lastRating Integer|Boolean: Value of the last Rating
*/
- private function insertRevisionRating( $pageId, $revisionId,
$lastRevision, $ratingId, $updateAddition, $thisRating, $lastRating ) {
+ private function insertRevisionRating( $pageId, $revisionId, $ratingId,
$updateAddition, $thisRating, $lastRating ) {
$dbw = wfGetDB( DB_MASTER );
// Try to insert a new "totals" row for this page,rev,rating set
@@ -179,58 +181,21 @@
__METHOD__,
array( 'IGNORE' )
);
-
- // If that succeded in inserting a row, then we are for sure
rating a previously unrated
- // revision, and we need to add more information about this
rating to the new "totals" row,
- // as well as remove the previous rating values from the
previous "totals" row
- if ( $dbw->affectedRows() ) {
- // If there was a previous rating, there should be a
"totals" row for it's revision
- if ( $lastRating ) {
- // Deduct the previous rating values from the
previous "totals" row
- $dbw->update(
- 'article_feedback_revisions',
- array(
- 'afr_total = afr_total - ' .
intval( $lastRating ),
- 'afr_count = afr_count - 1',
- ),
- array(
- 'afr_page_id' => $pageId,
- 'afr_rating_id' => $ratingId,
- 'afr_revision' => $lastRevision
- ),
- __METHOD__
- );
- }
- // Add this rating's values to the new "totals" row
- $dbw->update(
- 'article_feedback_revisions',
- array(
- 'afr_total' => $thisRating,
- 'afr_count' => $thisRating ? 1 : 0,
- ),
- array(
- 'afr_page_id' => $pageId,
- 'afr_rating_id' => $ratingId,
- 'afr_revision' => $revisionId,
- ),
- __METHOD__
- );
- } else {
- // Apply the difference between the previous and new
ratings to the current "totals" row
- $dbw->update(
- 'article_feedback_revisions',
- array(
- 'afr_total = afr_total + ' .
$updateAddition,
- 'afr_count = afr_count + ' .
$this->getCountChange( $lastRating, $thisRating ),
- ),
- array(
- 'afr_page_id' => $pageId,
- 'afr_rating_id' => $ratingId,
- 'afr_revision' => $revisionId,
- ),
- __METHOD__
- );
- }
+
+ // Apply the difference between the previous and new ratings to
the current "totals" row
+ $dbw->update(
+ 'article_feedback_revisions',
+ array(
+ 'afr_total = afr_total + ' . $updateAddition,
+ 'afr_count = afr_count + ' .
$this->getCountChange( $lastRating, $thisRating ),
+ ),
+ array(
+ 'afr_page_id' => $pageId,
+ 'afr_rating_id' => $ratingId,
+ 'afr_revision' => $revisionId,
+ ),
+ __METHOD__
+ );
}
/**
* Calculate the difference between the previous rating and this one
@@ -247,55 +212,45 @@
}
/**
- * Inserts (or Updates, where appropriate) the users ratings for a
specific revision
+ * Inserts the user's ratings for a specific revision
*
* @param $pageId Integer: Page Id
* @param $revisionId Integer: Revision Id
* @param $user User: Current User object
* @param $token Array: Token if necessary
- * @param $ratingId Integer: Rating Id
- * @param $ratingValue Integer: Value of the Rating
+ * @param $ratings Array: Keys are rating IDs, values are rating values
* @param $bucket Integer: Which rating widget was the user shown
+ * @return int Return value of $dbw->insertID(). This is the aa_id of
the first (MySQL) or last (SQLite) inserted row
*/
- private function insertUserRatings( $pageId, $revisionId, $user,
$token, $ratingId, $ratingValue, $bucket ) {
+ private function insertUserRatings( $pageId, $revisionId, $user,
$token, $ratings, $bucket ) {
$dbw = wfGetDB( DB_MASTER );
$timestamp = $dbw->timestamp();
-
- $dbw->insert(
- 'article_feedback',
- array(
+
+ $rows = array();
+ foreach ( $ratings as $ratingID => $ratingValue ) {
+ $rows[] = array(
'aa_page_id' => $pageId,
'aa_user_id' => $user->getId(),
'aa_user_text' => $user->getName(),
'aa_user_anon_token' => $token,
'aa_revision' => $revisionId,
'aa_timestamp' => $timestamp,
- 'aa_rating_id' => $ratingId,
+ 'aa_rating_id' => $ratingID,
'aa_rating_value' => $ratingValue,
'aa_design_bucket' => $bucket,
- ),
- __METHOD__,
- array( 'IGNORE' )
- );
-
- if ( !$dbw->affectedRows() ) {
- $dbw->update(
- 'article_feedback',
- array(
- 'aa_timestamp' => $timestamp,
- 'aa_rating_value' => $ratingValue,
- ),
- array(
- 'aa_page_id' => $pageId,
- 'aa_user_text' => $user->getName(),
- 'aa_revision' => $revisionId,
- 'aa_rating_id' => $ratingId,
- 'aa_user_anon_token' => $token,
- ),
- __METHOD__
);
}
+
+ // In MySQL, there is native support for multi-row inserts and
our rows
+ // will automatically get consecutive insertIDs. In SQLite this
seems
+ // to be the case if you use a transaction, but I couldn't find
anything
+ // on the web that states whether this is true.
+ $dbw->begin();
+ $dbw->insert( 'article_feedback', $rows, __METHOD__ );
+ $dbw->commit();
+
+ return $dbw->insertID();
}
/**
Modified: trunk/extensions/ArticleFeedback/api/ApiQueryArticleFeedback.php
===================================================================
--- trunk/extensions/ArticleFeedback/api/ApiQueryArticleFeedback.php
2011-09-10 13:04:07 UTC (rev 96721)
+++ trunk/extensions/ArticleFeedback/api/ApiQueryArticleFeedback.php
2011-09-10 15:26:11 UTC (rev 96722)
@@ -187,14 +187,13 @@
array(
'aa_page_id' => $params['pageid'],
'aa_rating_id' => $wgArticleFeedbackRatings,
- 'aa_user_id' => $wgUser->getId(),
'aa_user_text' => $wgUser->getName(),
'aa_user_anon_token' => $this->getAnonToken(
$params ),
),
__METHOD__,
array(
'LIMIT' => count( $wgArticleFeedbackRatings ),
- 'ORDER BY' => 'aa_revision DESC',
+ 'ORDER BY' => 'aa_id DESC',
),
array(
'article_feedback_ratings' => array( 'LEFT
JOIN', array( 'aar_id=aa_rating_id' ) )
Modified: trunk/extensions/ArticleFeedback/sql/ArticleFeedback.sql
===================================================================
--- trunk/extensions/ArticleFeedback/sql/ArticleFeedback.sql 2011-09-10
13:04:07 UTC (rev 96721)
+++ trunk/extensions/ArticleFeedback/sql/ArticleFeedback.sql 2011-09-10
15:26:11 UTC (rev 96722)
@@ -14,6 +14,8 @@
-- Store article feedbacks (user rating per revision)
CREATE TABLE IF NOT EXISTS /*_*/article_feedback (
+ -- Row ID (primary key)
+ aa_id integer unsigned NOT NULL PRIMARY KEY AUTO_INCREMENT,
-- Foreign key to page.page_id
aa_page_id integer unsigned NOT NULL,
-- User Id (0 if anon)
@@ -31,11 +33,10 @@
-- Value of the rating (0 is "unrated", else 1-5)
aa_rating_value int unsigned NOT NULL,
-- Which rating widget the user was given. Default of 0 is the "old" design
- aa_design_bucket int unsigned NOT NULL DEFAULT 0,
- -- 1 vote per user per revision
- PRIMARY KEY (aa_revision, aa_user_text, aa_rating_id, aa_user_anon_token)
+ aa_design_bucket int unsigned NOT NULL DEFAULT 0
) /*$wgDBTableOptions*/;
-CREATE INDEX /*i*/aa_user_page_revision ON /*_*/article_feedback (aa_user_id,
aa_page_id, aa_revision);
+CREATE INDEX /*i*/aa_page_user_token_id ON /*_*/article_feedback (aa_page_id,
aa_user_text, aa_user_anon_token, aa_id);
+CREATE INDEX /*i*/aa_revision ON /*_*/article_feedback (aa_revision);
-- Create an index on the article_feedback.aa_timestamp field
CREATE INDEX /*i*/article_feedback_timestamp ON /*_*/article_feedback
(aa_timestamp);
Added: trunk/extensions/ArticleFeedback/sql/RecreatePK.sql
===================================================================
--- trunk/extensions/ArticleFeedback/sql/RecreatePK.sql
(rev 0)
+++ trunk/extensions/ArticleFeedback/sql/RecreatePK.sql 2011-09-10 15:26:11 UTC
(rev 96722)
@@ -0,0 +1,38 @@
+-- Add aa_id as the new primary key to article_feedback and drop the old one.
+-- Also change the indexing while we're at it
+
+-- In order to safely change the primary key even in replicated environments,
+-- we have to create a new table with the new structure, copy over the data,
+-- then rename the table. This is to ensure that the values of aa_id are
+-- consistent across all slaves.
+
+-- Create new table
+-- Would've used CREATE TABLE ... LIKE here but SQLite doesn't support ALTER
TABLE ... DROP PRIMARY KEY
+-- so we're stuck with duplicating the table structure.
+CREATE TABLE /*_*/article_feedback2 (
+ aa_id integer unsigned NOT NULL PRIMARY KEY AUTO_INCREMENT,
+ aa_page_id integer unsigned NOT NULL,
+ aa_user_id integer NOT NULL,
+ aa_user_text varbinary(255) NOT NULL,
+ aa_user_anon_token varbinary(32) NOT NULL DEFAULT '',
+ aa_revision integer unsigned NOT NULL,
+ aa_timestamp binary(14) NOT NULL DEFAULT '',
+ aa_rating_id int unsigned NOT NULL,
+ aa_rating_value int unsigned NOT NULL,
+ aa_design_bucket int unsigned NOT NULL DEFAULT 0
+) /*$wgDBTableOptions*/;
+CREATE INDEX /*i*/aa_page_user_token_id ON /*_*/article_feedback2 (aa_page_id,
aa_user_text, aa_user_anon_token, aa_id);
+CREATE INDEX /*i*/aa_revision ON /*_*/article_feedback2 (aa_revision);
+CREATE INDEX /*i*/article_feedback_timestamp ON /*_*/article_feedback2
(aa_timestamp);
+
+-- Copy the data, ordered by the old primary key
+-- Need to specify the fields explicitly to avoid confusion with aa_id
+INSERT INTO /*_*/article_feedback2
+ (aa_page_id, aa_user_id, aa_user_text, aa_user_anon_token, aa_revision,
aa_timestamp, aa_rating_id, aa_rating_value, aa_design_bucket)
+ SELECT aa_page_id, aa_user_id, aa_user_text, aa_user_anon_token,
aa_revision, aa_timestamp, aa_rating_id, aa_rating_value, aa_design_bucket
+ FROM /*_*/article_feedback
+ ORDER BY aa_revision, aa_user_text, aa_rating_id, aa_user_anon_token;
+
+-- Drop the old table and rename the new table to the old name
+DROP TABLE /*_*/article_feedback;
+ALTER TABLE /*_*/article_feedback2 RENAME TO /*_*/article_feedback;
Property changes on: trunk/extensions/ArticleFeedback/sql/RecreatePK.sql
___________________________________________________________________
Added: svn:eol-style
+ native
_______________________________________________
MediaWiki-CVS mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-cvs