r108584 MediaWiki - Code Review archive

Repository:MediaWiki
Revision:r108583‎ | r108584 | r108585 >
Date:01:10, 11 January 2012
Author:raindrift
Status:reverted
Tags:
Comment:
Fixed concurrency issues related to mysql default locking mode, per Roan's comments. Fixed other little bugs Roan pointed out also.
followup to r108559
Modified paths:
  • /trunk/phase3/includes/ConcurrencyCheck.php (modified) (history)
  • /trunk/phase3/includes/DefaultSettings.php (modified) (history)
  • /trunk/phase3/tests/phpunit/includes/ConcurrencyCheckTest.php (modified) (history)

Diff [purge]

Index: trunk/phase3/tests/phpunit/includes/ConcurrencyCheckTest.php
@@ -28,9 +28,10 @@
2929
3030 // turn on memcached for this test.
3131 // if no memcached is present, this still works fine.
32 - global $wgMainCacheType;
 32+ global $wgMainCacheType, $wgConcurrency;
3333 $this->oldcache = $wgMainCacheType;
3434 $wgMainCacheType = CACHE_MEMCACHED;
 35+ $wgConcurrency['ExpirationMin'] = -60; // negative numbers are needed for testing
3536 }
3637
3738 public function tearDown() {
Index: trunk/phase3/includes/ConcurrencyCheck.php
@@ -54,16 +54,16 @@
5555 * @return boolean
5656 */
5757 public function checkout( $record, $override = null ) {
58 - $memc = wfGetMainCache();
 58+ global $wgMemc;
5959 $this->validateId( $record );
6060 $dbw = $this->dbw;
6161 $userId = $this->user->getId();
62 - $cacheKey = wfMemcKey( $this->resourceType, $record );
 62+ $cacheKey = wfMemcKey( 'concurrencycheck', $this->resourceType, $record );
6363
6464 // when operating with a single memcached cluster, it's reasonable to check the cache here.
65 - global $wgConcurrencyTrustMemc;
66 - if( $wgConcurrencyTrustMemc ) {
67 - $cached = $memc->get( $cacheKey );
 65+ global $wgConcurrency;
 66+ if( $wgConcurrency['TrustMemc'] ) {
 67+ $cached = $wgMemc->get( $cacheKey );
6868 if( $cached ) {
6969 if( ! $override && $cached['userId'] != $userId && $cached['expiration'] > time() ) {
7070 // this is already checked out.
@@ -90,39 +90,54 @@
9191 if( $dbw->affectedRows() === 1 ) {
9292 // delete any existing cache key. can't create a new key here
9393 // since the insert didn't happen inside a transaction.
94 - $memc->delete( $cacheKey );
 94+ $wgMemc->delete( $cacheKey );
9595 return true;
9696 }
9797
9898 // if the insert failed, it's necessary to check the expiration.
 99+ // here, check by deleting, since that permits the use of write locks
 100+ // (SELECT..LOCK IN SHARE MODE), rather than read locks (SELECT..FOR UPDATE)
99101 $dbw->begin();
100 - $row = $dbw->selectRow(
 102+ $dbw->delete(
101103 'concurrencycheck',
102 - array( 'cc_user', 'cc_expiration' ),
103104 array(
104105 'cc_resource_type' => $this->resourceType,
105106 'cc_record' => $record,
 107+ '(cc_user = ' . $userId . ' OR cc_expiration <= ' . wfTimestamp( TS_MW ) . ')', // only the owner can perform a checkin
106108 ),
107109 __METHOD__,
108110 array()
109111 );
110 -
111 - // not checked out by current user, checkout is unexpired, override is unset
112 - if( ! ( $override || $row->cc_user == $userId || wfTimestamp( TS_UNIX, $row->cc_expiration ) <= time() ) ) {
 112+
 113+ // delete failed: not checked out by current user, checkout is unexpired, override is unset
 114+ if( $dbw->affectedRows() !== 1 && ! $override) {
 115+ // fetch the existing data to cache it
 116+ $row = $dbw->selectRow(
 117+ 'concurrencycheck',
 118+ array( '*' ),
 119+ array(
 120+ 'cc_resource_type' => $this->resourceType,
 121+ 'cc_record' => $record,
 122+ ),
 123+ __METHOD__,
 124+ array()
 125+ );
 126+
113127 // this was a cache miss. populate the cache with data from the db.
114128 // cache is set to expire at the same time as the checkout, since it'll become invalid then anyway.
115129 // inside this transaction, a row-level lock is established which ensures cache concurrency
116 - $memc->set( $cacheKey, array( 'userId' => $row->cc_user, 'expiration' => $row->cc_expiration ), wfTimestamp( TS_UNIX, $row->cc_expiration ) - time() );
 130+ $wgMemc->set( $cacheKey, array( 'userId' => $row->cc_user, 'expiration' => $row->cc_expiration ), wfTimestamp( TS_UNIX, $row->cc_expiration ) - time() );
117131 $dbw->rollback();
118132 return false;
119133 }
120134
121135 $expiration = time() + $this->expirationTime;
122136
123 - // execute a replace
 137+ // delete succeeded, insert a new row.
 138+ // replace is used here to support the override parameter
124139 $res = $dbw->replace(
125140 'concurrencycheck',
126 - array( array('cc_resource_type', 'cc_record') ),
 141+ array( 'cc_resource_type', 'cc_record' ),
127142 array(
128143 'cc_resource_type' => $this->resourceType,
129144 'cc_record' => $record,
@@ -133,7 +148,7 @@
134149 );
135150
136151 // cache the result.
137 - $memc->set( $cacheKey, array( 'userId' => $userId, 'expiration' => $expiration ), $this->expirationTime );
 152+ $wgMemc->set( $cacheKey, array( 'userId' => $userId, 'expiration' => $expiration ), $this->expirationTime );
138153
139154 $dbw->commit();
140155 return true;
@@ -146,11 +161,11 @@
147162 * @return Boolean
148163 */
149164 public function checkin( $record ) {
150 - $memc = wfGetMainCache();
 165+ global $wgMemc;
151166 $this->validateId( $record );
152167 $dbw = $this->dbw;
153168 $userId = $this->user->getId();
154 - $cacheKey = wfMemcKey( $this->resourceType, $record );
 169+ $cacheKey = wfMemcKey( 'concurrencycheck', $this->resourceType, $record );
155170
156171 $dbw->delete(
157172 'concurrencycheck',
@@ -165,7 +180,7 @@
166181
167182 // check row count (this is atomic, select would not be)
168183 if( $dbw->affectedRows() === 1 ) {
169 - $memc->delete( $cacheKey );
 184+ $wgMemc->delete( $cacheKey );
170185 return true;
171186 }
172187
@@ -178,28 +193,11 @@
179194 * @return Integer describing the number of records expired.
180195 */
181196 public function expire() {
182 - $memc = wfGetMainCache();
 197+ // TODO: run this in a few other places that db access happens, to make sure the db stays non-crufty.
183198 $dbw = $this->dbw;
184199 $now = time();
185200
186 - // get the rows to remove from cache.
187 - $res = $dbw->select(
188 - 'concurrencycheck',
189 - array( '*' ),
190 - array(
191 - 'cc_expiration <= ' . $dbw->addQuotes( wfTimestamp( TS_MW, $now ) ),
192 - ),
193 - __METHOD__,
194 - array()
195 - );
196 -
197 - // build a list of rows to delete.
198 - $toExpire = array();
199 - while( $res && $record = $res->fetchRow() ) {
200 - $toExpire[] = $record['cc_record'];
201 - }
202 -
203 - // remove the rows from the db
 201+ // remove the rows from the db. trust memcached to expire the cache.
204202 $dbw->delete(
205203 'concurrencycheck',
206204 array(
@@ -209,18 +207,12 @@
210208 array()
211209 );
212210
213 - // delete all those rows from cache
214 - // outside a transaction because deletes don't require atomicity.
215 - foreach( $toExpire as $expire ) {
216 - $memc->delete( wfMemcKey( $this->resourceType, $expire ) );
217 - }
218 -
219211 // return the number of rows removed.
220212 return $dbw->affectedRows();
221213 }
222214
223215 public function status( $keys ) {
224 - $memc = wfGetMainCache();
 216+ global $wgMemc;
225217 $dbw = $this->dbw;
226218 $now = time();
227219
@@ -231,7 +223,7 @@
232224 foreach( $keys as $key ) {
233225 $this->validateId( $key );
234226
235 - $cached = $memc->get( wfMemcKey( $this->resourceType, $key ) );
 227+ $cached = $wgMemc->get( wfMemcKey( 'concurrencycheck', $this->resourceType, $key ) );
236228 if( $cached && $cached['expiration'] > $now ) {
237229 $checkouts[$key] = array(
238230 'status' => 'valid',
@@ -253,16 +245,29 @@
254246
255247 // the transaction seems incongruous, I know, but it's to keep the cache update atomic.
256248 $dbw->begin();
 249+
 250+ // Why LOCK IN SHARE MODE, you might ask? To avoid a race condition: Otherwise, it's possible for
 251+ // a checkin and/or checkout to occur between this select and the value being stored in cache, which
 252+ // makes for an incorrect cache. This, in turn, could make checkout() above (which uses the cache)
 253+ // function incorrectly.
 254+ //
 255+ // Another option would be to run the select, then check each row in-turn before setting the cache
 256+ // key using either SELECT (with LOCK IN SHARE MODE) or UPDATE that checks a timestamp (and which
 257+ // would establish the same lock). That method would mean smaller, quicker locks, but more overall
 258+ // database overhead.
 259+ //
 260+ // It appears all the DBMSes we use support LOCK IN SHARE MODE, but if that's not the case, the second
 261+ // solution above could be implemented instead.
257262 $res = $dbw->select(
258263 'concurrencycheck',
259264 array( '*' ),
260265 array(
261266 'cc_resource_type' => $this->resourceType,
262 - 'cc_record IN (' . implode( ',', $toSelect ) . ')',
 267+ 'cc_record' => $toSelect,
263268 'cc_expiration > ' . $dbw->addQuotes( wfTimestamp( TS_MW ) ),
264269 ),
265270 __METHOD__,
266 - array()
 271+ array('LOCK IN SHARE MODE')
267272 );
268273
269274 while( $res && $record = $res->fetchRow() ) {
@@ -270,8 +275,8 @@
271276 $checkouts[ $record['cc_record'] ] = $record;
272277
273278 // safe to store values since this is inside the transaction
274 - $memc->set(
275 - wfMemcKey( $this->resourceType, $record['cc_record'] ),
 279+ $wgMemc->set(
 280+ wfMemcKey( 'concurrencycheck', $this->resourceType, $record['cc_record'] ),
276281 array( 'userId' => $record['cc_user'], 'expiration' => $record['cc_expiration'] ),
277282 $record['cc_expiration'] - time()
278283 );
@@ -304,20 +309,20 @@
305310 }
306311
307312 public function setExpirationTime( $expirationTime = null ) {
308 - global $wgConcurrencyExpirationDefault, $wgConcurrencyExpirationMax, $wgConcurrencyExpirationMin;
309 -
310 - // check to make sure the time is digits only, so it can be used in queries
 313+ global $wgConcurrency;
 314+
 315+ // check to make sure the time is a number
311316 // negative number are allowed, though mostly only used for testing
312 - if( $expirationTime && preg_match('/^[\d-]+$/', $expirationTime) ) {
313 - if( $expirationTime > $wgConcurrencyExpirationMax ) {
314 - $this->expirationTime = $wgConcurrencyExpirationMax; // if the number is too high, limit it to the max value.
315 - } elseif ( $expirationTime < $wgConcurrencyExpirationMin ) {
316 - $this->expirationTime = $wgConcurrencyExpirationMin; // low limit, default -1 min
 317+ if( $expirationTime && (int) $expirationTime == $expirationTime ) {
 318+ if( $expirationTime > $wgConcurrency['ExpirationMax'] ) {
 319+ $this->expirationTime = $wgConcurrency['ExpirationMax']; // if the number is too high, limit it to the max value.
 320+ } elseif ( $expirationTime < $wgConcurrency['ExpirationMin'] ) {
 321+ $this->expirationTime = $wgConcurrency['ExpirationMin']; // low limit, default -1 min
317322 } else {
318323 $this->expirationTime = $expirationTime; // the amount of time before a checkout expires.
319324 }
320325 } else {
321 - $this->expirationTime = $wgConcurrencyExpirationDefault; // global default is 15 mins.
 326+ $this->expirationTime = $wgConcurrency['ExpirationDefault']; // global default is 15 mins.
322327 }
323328 }
324329
@@ -329,7 +334,7 @@
330335 * @return boolean
331336 */
332337 private static function validateId ( $record ) {
333 - if( ! preg_match('/^\d+$/', $record) ) {
 338+ if( (int) $record !== $record || $record <= 0 ) {
334339 throw new ConcurrencyCheckBadRecordIdException( 'Record ID ' . $record . ' must be a positive integer' );
335340 }
336341
Index: trunk/phase3/includes/DefaultSettings.php
@@ -5719,10 +5719,11 @@
57205720 /**
57215721 * ConcurrencyCheck keeps track of which web resources are in use, for producing higher-quality UI
57225722 */
5723 -$wgConcurrencyExpirationDefault = 60 * 15; // Default checkout duration. 15 minutes.
5724 -$wgConcurrencyExpirationMax = 60 * 30; // Maximum possible checkout duration. 30 minutes.
5725 -$wgConcurrencyExpirationMin = 60 * -1; // Minimum possible checkout duration. Negative is possible (but barely) for testing.
5726 -$wgConcurrencyTrustMemc = true; // If running in an environment with multiple discrete caches, set to false.
 5723+$wgConcurrency = array();
 5724+$wgConcurrency['ExpirationDefault'] = 60 * 15; // Default checkout duration. 15 minutes.
 5725+$wgConcurrency['ExpirationMax'] = 60 * 30; // Maximum possible checkout duration. 30 minutes.
 5726+$wgConcurrency['ExpirationMin'] = 1; // Minimum possible checkout duration. Negative is possible for testing if you want.
 5727+$wgConcurrency['TrustMemc'] = true; // If running in an environment with multiple discrete caches, set to false.
57275728
57285729
57295730 /**

Follow-up revisions

RevisionCommit summaryAuthorDate
r108601reverts Concurrency works...hashar09:05, 11 January 2012

Past revisions this follows-up on

RevisionCommit summaryAuthorDate
r108559MERGE branches/concurrency 108301:108557 into trunkraindrift23:03, 10 January 2012

Status & tagging log