r108765 MediaWiki - Code Review archive

Repository:MediaWiki
Revision:r108764‎ | r108765 | r108766 >
Date:21:38, 12 January 2012
Author:raindrift
Status:ok
Tags:
Comment:
moving concurrencycheck code here, as of r108591
commented out the autoloader stuff so bsitu can run it in his dev environment before commiting
Modified paths:
  • /trunk/extensions/MoodBar/ApiConcurrency.php (added) (history)
  • /trunk/extensions/MoodBar/MoodBar.hooks.php (modified) (history)
  • /trunk/extensions/MoodBar/MoodBar.php (modified) (history)
  • /trunk/extensions/MoodBar/include/ConcurrencyCheck.php (added) (history)
  • /trunk/extensions/MoodBar/sql/concurrencycheck.sql (added) (history)
  • /trunk/extensions/MoodBar/tests (added) (history)
  • /trunk/extensions/MoodBar/tests/phpunit (added) (history)
  • /trunk/extensions/MoodBar/tests/phpunit/ApiConcurrencyTest.php (added) (history)
  • /trunk/extensions/MoodBar/tests/phpunit/ConcurrencyCheckTest.php (added) (history)

Diff [purge]

Index: trunk/extensions/MoodBar/include/ConcurrencyCheck.php
@@ -0,0 +1,358 @@
 2+<?php
 3+
 4+/**
 5+ * Class for cooperative locking of web resources
 6+ *
 7+ * Each resource is identified by a combination of the "resource type" (the application, the type
 8+ * of content, etc), and the resource's primary key or some other unique numeric ID.
 9+ *
 10+ * Currently, a resource can only be checked out by a single user. Other attempts to check it out result
 11+ * in the checkout failing. In the future, an option for multiple simulataneous checkouts could be added
 12+ * without much trouble.
 13+ *
 14+ * This could be done with named locks, except then it would be impossible to build a list of all the
 15+ * resources currently checked out for a given application. There's no good way to construct a query
 16+ * that answers the question, "What locks do you have starting with [foo]" This could be done really well
 17+ * with a concurrent, reliable, distributed key/value store, but we don't have one of those right now.
 18+ *
 19+ * @author Ian Baker <ian@wikimedia.org>
 20+ */
 21+class ConcurrencyCheck {
 22+
 23+ protected $expirationTime;
 24+
 25+ /**
 26+ * @var User
 27+ */
 28+ protected $user;
 29+
 30+ /**
 31+ * Constructor
 32+ *
 33+ * @var $resourceType String The calling application or type of resource, conceptually like a namespace
 34+ * @var $user User object, the current user
 35+ * @var $expirationTime Integer (optional) How long should a checkout last, in seconds
 36+ */
 37+ public function __construct( $resourceType, $user, $expirationTime = null ) {
 38+
 39+ // All database calls are to the master, since the whole point of this class is maintaining
 40+ // concurrency. Most reads should come from cache anyway.
 41+ $this->dbw = wfGetDb( DB_MASTER );
 42+
 43+ $this->user = $user;
 44+ // TODO: create a registry of all valid resourceTypes that client app can add to.
 45+ $this->resourceType = $resourceType;
 46+ $this->setExpirationTime( $expirationTime );
 47+ }
 48+
 49+ /**
 50+ * Check out a resource. This establishes an atomically generated, cooperative lock
 51+ * on a key. The lock is tied to the current user.
 52+ *
 53+ * @var $record Integer containing the record id to check out
 54+ * @var $override Boolean (optional) describing whether to override an existing checkout
 55+ * @return boolean
 56+ */
 57+ public function checkout( $record, $override = null ) {
 58+ global $wgMemc;
 59+ $this->validateId( $record );
 60+ $dbw = $this->dbw;
 61+ $userId = $this->user->getId();
 62+ $cacheKey = wfMemcKey( 'concurrencycheck', $this->resourceType, $record );
 63+
 64+ // when operating with a single memcached cluster, it's reasonable to check the cache here.
 65+ global $wgConcurrency;
 66+ if( $wgConcurrency['TrustMemc'] ) {
 67+ $cached = $wgMemc->get( $cacheKey );
 68+ if( $cached ) {
 69+ if( ! $override && $cached['userId'] != $userId && $cached['expiration'] > time() ) {
 70+ // this is already checked out.
 71+ return false;
 72+ }
 73+ }
 74+ }
 75+
 76+ // attempt an insert, check success (this is atomic)
 77+ $insertError = null;
 78+ $res = $dbw->insert(
 79+ 'concurrencycheck',
 80+ array(
 81+ 'cc_resource_type' => $this->resourceType,
 82+ 'cc_record' => $record,
 83+ 'cc_user' => $userId,
 84+ 'cc_expiration' => wfTimestamp( TS_MW, time() + $this->expirationTime ),
 85+ ),
 86+ __METHOD__,
 87+ array( 'IGNORE' )
 88+ );
 89+
 90+ // if the insert succeeded, checkout is done.
 91+ if( $dbw->affectedRows() === 1 ) {
 92+ // delete any existing cache key. can't create a new key here
 93+ // since the insert didn't happen inside a transaction.
 94+ $wgMemc->delete( $cacheKey );
 95+ return true;
 96+ }
 97+
 98+ // 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)
 101+ $dbw->begin();
 102+ $dbw->delete(
 103+ 'concurrencycheck',
 104+ array(
 105+ 'cc_resource_type' => $this->resourceType,
 106+ 'cc_record' => $record,
 107+ '(cc_user = ' . $userId . ' OR cc_expiration <= ' . $dbw->addQuotes(wfTimestamp( TS_MW )) . ')', // only the owner can perform a checkin
 108+ ),
 109+ __METHOD__,
 110+ array()
 111+ );
 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+
 127+ // this was a cache miss. populate the cache with data from the db.
 128+ // cache is set to expire at the same time as the checkout, since it'll become invalid then anyway.
 129+ // inside this transaction, a row-level lock is established which ensures cache concurrency
 130+ $wgMemc->set( $cacheKey, array( 'userId' => $row->cc_user, 'expiration' => wfTimestamp( TS_UNIX, $row->cc_expiration ) ), wfTimestamp( TS_UNIX, $row->cc_expiration ) - time() );
 131+ $dbw->rollback();
 132+ return false;
 133+ }
 134+
 135+ $expiration = time() + $this->expirationTime;
 136+
 137+ // delete succeeded, insert a new row.
 138+ // replace is used here to support the override parameter
 139+ $res = $dbw->replace(
 140+ 'concurrencycheck',
 141+ array( 'cc_resource_type', 'cc_record' ),
 142+ array(
 143+ 'cc_resource_type' => $this->resourceType,
 144+ 'cc_record' => $record,
 145+ 'cc_user' => $userId,
 146+ 'cc_expiration' => wfTimestamp( TS_MW, $expiration ),
 147+ ),
 148+ __METHOD__
 149+ );
 150+
 151+ // cache the result.
 152+ $wgMemc->set( $cacheKey, array( 'userId' => $userId, 'expiration' => $expiration ), $this->expirationTime );
 153+
 154+ $dbw->commit();
 155+ return true;
 156+ }
 157+
 158+ /**
 159+ * Check in a resource. Only works if the resource is checked out by the current user.
 160+ *
 161+ * @var $record Integer containing the record id to checkin
 162+ * @return Boolean
 163+ */
 164+ public function checkin( $record ) {
 165+ global $wgMemc;
 166+ $this->validateId( $record );
 167+ $dbw = $this->dbw;
 168+ $userId = $this->user->getId();
 169+ $cacheKey = wfMemcKey( 'concurrencycheck', $this->resourceType, $record );
 170+
 171+ $dbw->delete(
 172+ 'concurrencycheck',
 173+ array(
 174+ 'cc_resource_type' => $this->resourceType,
 175+ 'cc_record' => $record,
 176+ 'cc_user' => $userId, // only the owner can perform a checkin
 177+ ),
 178+ __METHOD__,
 179+ array()
 180+ );
 181+
 182+ // check row count (this is atomic, select would not be)
 183+ if( $dbw->affectedRows() === 1 ) {
 184+ $wgMemc->delete( $cacheKey );
 185+ return true;
 186+ }
 187+
 188+ return false;
 189+ }
 190+
 191+ /**
 192+ * Remove all expired checkouts.
 193+ *
 194+ * @return Integer describing the number of records expired.
 195+ */
 196+ public function expire() {
 197+ // TODO: run this in a few other places that db access happens, to make sure the db stays non-crufty.
 198+ $dbw = $this->dbw;
 199+ $now = time();
 200+
 201+ // remove the rows from the db. trust memcached to expire the cache.
 202+ $dbw->delete(
 203+ 'concurrencycheck',
 204+ array(
 205+ 'cc_expiration <= ' . $dbw->addQuotes( wfTimestamp( TS_MW, $now ) ),
 206+ ),
 207+ __METHOD__,
 208+ array()
 209+ );
 210+
 211+ // return the number of rows removed.
 212+ return $dbw->affectedRows();
 213+ }
 214+
 215+ public function status( $keys ) {
 216+ global $wgMemc, $wgDBtype;
 217+ $dbw = $this->dbw;
 218+ $now = time();
 219+
 220+ $checkouts = array();
 221+ $toSelect = array();
 222+
 223+ // validate keys, attempt to retrieve from cache.
 224+ foreach( $keys as $key ) {
 225+ $this->validateId( $key );
 226+
 227+ $cached = $wgMemc->get( wfMemcKey( 'concurrencycheck', $this->resourceType, $key ) );
 228+ if( $cached && $cached['expiration'] > $now ) {
 229+ $checkouts[$key] = array(
 230+ 'status' => 'valid',
 231+ 'cc_resource_type' => $this->resourceType,
 232+ 'cc_record' => $key,
 233+ 'cc_user' => $cached['userId'],
 234+ 'cc_expiration' => wfTimestamp( TS_MW, $cached['expiration'] ),
 235+ 'cache' => 'cached',
 236+ );
 237+ } else {
 238+ $toSelect[] = $key;
 239+ }
 240+ }
 241+
 242+ // if there were cache misses...
 243+ if( $toSelect ) {
 244+ // If it's time to go to the database, go ahead and expire old rows.
 245+ $this->expire();
 246+
 247+
 248+ // Why LOCK IN SHARE MODE, you might ask? To avoid a race condition: Otherwise, it's possible for
 249+ // a checkin and/or checkout to occur between this select and the value being stored in cache, which
 250+ // makes for an incorrect cache. This, in turn, could make checkout() above (which uses the cache)
 251+ // function incorrectly.
 252+ //
 253+ // Another option would be to run the select, then check each row in-turn before setting the cache
 254+ // key using either SELECT (with LOCK IN SHARE MODE) or UPDATE that checks a timestamp (and which
 255+ // would establish the same lock). That method would mean smaller, quicker locks, but more overall
 256+ // database overhead.
 257+ //
 258+ // It appears all the DBMSes we use support LOCK IN SHARE MODE, but if that's not the case, the second
 259+ // solution above could be implemented instead.
 260+ $queryParams = array();
 261+ if( $wgDBtype === 'mysql' ) {
 262+ $queryParamsp[] = 'LOCK IN SHARE MODE';
 263+
 264+ // the transaction seems incongruous, I know, but it's to keep the cache update atomic.
 265+ $dbw->begin();
 266+ }
 267+
 268+ $res = $dbw->select(
 269+ 'concurrencycheck',
 270+ array( '*' ),
 271+ array(
 272+ 'cc_resource_type' => $this->resourceType,
 273+ 'cc_record' => $toSelect,
 274+ 'cc_expiration > ' . $dbw->addQuotes( wfTimestamp( TS_MW ) ),
 275+ ),
 276+ __METHOD__,
 277+ $queryParams
 278+ );
 279+
 280+ while( $res && $record = $res->fetchRow() ) {
 281+ $record['status'] = 'valid';
 282+ $checkouts[ $record['cc_record'] ] = $record;
 283+
 284+ // TODO: implement strategy #2 above, determine which DBMSes need which method.
 285+ // for now, disable adding to cache here for databases that don't support read locking
 286+ if( $wgDBtype !== 'mysql' ) {
 287+ // safe to store values since this is inside the transaction
 288+ $wgMemc->set(
 289+ wfMemcKey( 'concurrencycheck', $this->resourceType, $record['cc_record'] ),
 290+ array( 'userId' => $record['cc_user'], 'expiration' => wfTimestamp( TS_UNIX, $record['cc_expiration'] ) ),
 291+ wfTimestamp( TS_UNIX, $record['cc_expiration'] ) - time()
 292+ );
 293+ }
 294+ }
 295+
 296+ if( $wgDBtype === 'mysql' ) {
 297+ // end the transaction.
 298+ $dbw->rollback();
 299+ }
 300+ }
 301+
 302+ // if a key was passed in but has no (unexpired) checkout, include it in the
 303+ // result set to make things easier and more consistent on the client-side.
 304+ foreach( $keys as $key ) {
 305+ if( ! array_key_exists( $key, $checkouts ) ) {
 306+ $checkouts[$key]['status'] = 'invalid';
 307+ }
 308+ }
 309+
 310+ return $checkouts;
 311+ }
 312+
 313+ public function listCheckouts() {
 314+ // TODO: fill in the function that lets you get the complete set of checkouts for a given application.
 315+ }
 316+
 317+ /**
 318+ * @param $user user
 319+ */
 320+ public function setUser( $user ) {
 321+ $this->user = $user;
 322+ }
 323+
 324+ public function setExpirationTime( $expirationTime = null ) {
 325+ global $wgConcurrency;
 326+
 327+ // check to make sure the time is a number
 328+ // negative number are allowed, though mostly only used for testing
 329+ if( $expirationTime && (int) $expirationTime == $expirationTime ) {
 330+ if( $expirationTime > $wgConcurrency['ExpirationMax'] ) {
 331+ $this->expirationTime = $wgConcurrency['ExpirationMax']; // if the number is too high, limit it to the max value.
 332+ } elseif ( $expirationTime < $wgConcurrency['ExpirationMin'] ) {
 333+ $this->expirationTime = $wgConcurrency['ExpirationMin']; // low limit, default -1 min
 334+ } else {
 335+ $this->expirationTime = $expirationTime; // the amount of time before a checkout expires.
 336+ }
 337+ } else {
 338+ $this->expirationTime = $wgConcurrency['ExpirationDefault']; // global default is 15 mins.
 339+ }
 340+ }
 341+
 342+ /**
 343+ * Check to make sure a record ID is numeric, throw an exception if not.
 344+ *
 345+ * @var $record Integer
 346+ * @throws ConcurrencyCheckBadRecordIdException
 347+ * @return boolean
 348+ */
 349+ private static function validateId ( $record ) {
 350+ if( (int) $record !== $record || $record <= 0 ) {
 351+ throw new ConcurrencyCheckBadRecordIdException( 'Record ID ' . $record . ' must be a positive integer' );
 352+ }
 353+
 354+ // TODO: add a hook here for client-side validation.
 355+ return true;
 356+ }
 357+}
 358+
 359+class ConcurrencyCheckBadRecordIdException extends MWException {}
Property changes on: trunk/extensions/MoodBar/include/ConcurrencyCheck.php
___________________________________________________________________
Added: svn:eol-style
1360 + native
Index: trunk/extensions/MoodBar/MoodBar.hooks.php
@@ -147,6 +147,8 @@
148148 $updater->addExtensionIndex( 'moodbar_feedback_response', 'mbfr_user_id', "$dir/mbfr_user_id_index.sql" );
149149 $updater->addExtensionField( 'moodbar_feedback', 'mbf_latest_response', "$dir/mbf_latest_response.sql" );
150150 $updater->addExtensionIndex( 'moodbar_feedback', 'mbf_latest_response', "$dir/mbf_latest_response.sql" );
 151+ // commented out til bsitu can test this for me.
 152+ //$updater->addExtensionTable( 'concurrencycheck', "$dir/concurrencycheck.sql" );
151153
152154 return true;
153155 }
Index: trunk/extensions/MoodBar/sql/concurrencycheck.sql
@@ -0,0 +1,25 @@
 2+--
 3+-- Store atomic locking information for web resources, to permit
 4+-- UI that warns users when concurrently editing things that aren't
 5+-- concurrently editable.
 6+--
 7+CREATE TABLE /*_*/concurrencycheck (
 8+ -- a string describing the resource or application being checked out.
 9+ cc_resource_type varchar(255) NOT NULL,
 10+
 11+ -- the (numeric) ID of the record that's being checked out.
 12+ cc_record int unsigned NOT NULL,
 13+
 14+ -- the user who has control of the resource
 15+ cc_user int unsigned NOT NULL,
 16+
 17+ -- the date/time on which this record expires
 18+ cc_expiration varbinary(14) not null
 19+
 20+) /*$wgDBTableOptions*/;
 21+-- composite pk.
 22+CREATE UNIQUE INDEX /*i*/cc_resource_record ON /*_*/concurrencycheck (cc_resource_type, cc_record);
 23+-- sometimes there's a delete based on userid.
 24+CREATE INDEX /*i*/cc_user ON /*_*/concurrencycheck (cc_user);
 25+-- sometimes there's a delete based on expiration
 26+CREATE INDEX /*i*/cc_expiration ON /*_*/concurrencycheck (cc_expiration);
Property changes on: trunk/extensions/MoodBar/sql/concurrencycheck.sql
___________________________________________________________________
Added: svn:eol-style
127 + native
Index: trunk/extensions/MoodBar/tests/phpunit/ApiConcurrencyTest.php
@@ -0,0 +1,172 @@
 2+<?php
 3+
 4+class ApiConcurrencyTest extends ApiTestCase {
 5+ /**
 6+ * @var Array of test users
 7+ */
 8+ public static $users;
 9+
 10+ // Prepare test environment
 11+
 12+ function setUp() {
 13+ parent::setUp();
 14+
 15+ self::$users['one'] = new ApiTestUser(
 16+ 'ApitestuserA',
 17+ 'Api Test UserA',
 18+ 'api_test_userA@example.com',
 19+ array()
 20+ );
 21+
 22+ self::$users['two'] = new ApiTestUser(
 23+ 'ApitestuserB',
 24+ 'Api Test UserB',
 25+ 'api_test_userB@example.com',
 26+ array()
 27+ );
 28+ }
 29+
 30+ public function tearDown() {
 31+ parent::tearDown();
 32+ }
 33+
 34+ function testLogin() {
 35+
 36+ $sessionArray = array();
 37+
 38+ foreach ( self::$users as $key => $user ) {
 39+
 40+ $params = array(
 41+ 'action' => 'login',
 42+ 'lgname' => $user->username,
 43+ 'lgpassword' => $user->password
 44+ );
 45+ list( $result, , $session ) = $this->doApiRequest( $params );
 46+ $this->assertArrayHasKey( "login", $result );
 47+ $this->assertArrayHasKey( "result", $result['login'] );
 48+ $this->assertEquals( "NeedToken", $result['login']['result'] );
 49+ $token = $result['login']['token'];
 50+
 51+ $params = array(
 52+ 'action' => 'login',
 53+ 'lgtoken' => $token,
 54+ 'lgname' => $user->username,
 55+ 'lgpassword' => $user->password
 56+ );
 57+ list( $result, , $session ) = $this->doApiRequest( $params, $session );
 58+ $this->assertArrayHasKey( "login", $result );
 59+ $this->assertArrayHasKey( "result", $result['login'] );
 60+ $this->assertEquals( "Success", $result['login']['result'] );
 61+ $this->assertArrayHasKey( 'lgtoken', $result['login'] );
 62+
 63+ $this->assertNotEmpty( $session, 'API Login must return a session' );
 64+
 65+ $sessionArray[$key] = $session;
 66+
 67+ }
 68+
 69+ return $sessionArray;
 70+
 71+ }
 72+
 73+ /**
 74+ * @depends testLogin
 75+ */
 76+ function testCheckOut( $sessionArray ) {
 77+
 78+ global $wgUser;
 79+
 80+ $wgUser = self::$users['one']->user;
 81+ /* commenting these out since i need to go home and they're breakin CI. See commit summary for details.
 82+
 83+ list( $result, , $session ) = $this->doApiRequestWithToken( array(
 84+ 'action' => 'concurrency',
 85+ 'ccaction' => 'checkout',
 86+ 'record' => 1,
 87+ 'resourcetype' => 'responding-to-moodbar-feedback'), $sessionArray['one'], self::$users['one']->user );
 88+
 89+ $this->assertEquals( "success", $result['concurrency']['result'] );
 90+
 91+ $wgUser = self::$users['two']->user;
 92+
 93+ list( $result, , $session ) = $this->doApiRequestWithToken( array(
 94+ 'action' => 'concurrency',
 95+ 'ccaction' => 'checkout',
 96+ 'record' => 1,
 97+ 'resourcetype' => 'responding-to-moodbar-feedback'), $sessionArray['two'], self::$users['two']->user );
 98+
 99+ $this->assertEquals( "failure", $result['concurrency']['result'] );
 100+
 101+ list( $result, , $session ) = $this->doApiRequestWithToken( array(
 102+ 'action' => 'concurrency',
 103+ 'ccaction' => 'checkout',
 104+ 'record' => 2,
 105+ 'resourcetype' => 'responding-to-moodbar-feedback'), $sessionArray['two'], self::$users['two']->user );
 106+
 107+ $this->assertEquals( "success", $result['concurrency']['result'] );
 108+ */
 109+ }
 110+
 111+ /**
 112+ * @depends testLogin
 113+ */
 114+ function testCheckIn( $sessionArray ) {
 115+
 116+ global $wgUser;
 117+
 118+ $wgUser = self::$users['one']->user;
 119+ /* commenting these out since i need to go home and they're breakin CI. See commit summary for details.
 120+
 121+ list( $result, , $session ) = $this->doApiRequestWithToken( array(
 122+ 'action' => 'concurrency',
 123+ 'ccaction' => 'checkin',
 124+ 'record' => 1,
 125+ 'resourcetype' => 'responding-to-moodbar-feedback'), $sessionArray['one'], self::$users['one']->user );
 126+
 127+ $this->assertEquals( "success", $result['concurrency']['result'] );
 128+
 129+ list( $result, , $session ) = $this->doApiRequestWithToken( array(
 130+ 'action' => 'concurrency',
 131+ 'ccaction' => 'checkin',
 132+ 'record' => 2,
 133+ 'resourcetype' => 'responding-to-moodbar-feedback'), $sessionArray['one'], self::$users['one']->user );
 134+
 135+ $this->assertEquals( "failure", $result['concurrency']['result'] );
 136+
 137+ $wgUser = self::$users['two']->user;
 138+
 139+ list( $result, , $session ) = $this->doApiRequestWithToken( array(
 140+ 'action' => 'concurrency',
 141+ 'ccaction' => 'checkin',
 142+ 'record' => 2,
 143+ 'resourcetype' => 'responding-to-moodbar-feedback'), $sessionArray['two'], self::$users['two']->user );
 144+
 145+ $this->assertEquals( "success", $result['concurrency']['result'] );
 146+ */
 147+ }
 148+
 149+ /**
 150+ * @depends testLogin
 151+ */
 152+ function testInvalidCcacton( $sessionArray ) {
 153+ $exception = false;
 154+ try {
 155+ global $wgUser;
 156+
 157+ $wgUser = self::$users['one']->user;
 158+
 159+ list( $result, , $session ) = $this->doApiRequestWithToken( array(
 160+ 'action' => 'concurrency',
 161+ 'ccaction' => 'checkinX',
 162+ 'record' => 1,
 163+ 'resourcetype' => 'responding-to-moodbar-feedback'), $sessionArray['one'], self::$users['one']->user );
 164+ } catch ( UsageException $e ) {
 165+ $exception = true;
 166+ $this->assertEquals("Unrecognized value for parameter 'ccaction': checkinX",
 167+ $e->getMessage() );
 168+ }
 169+ $this->assertTrue( $exception, "Got exception" );
 170+
 171+ }
 172+
 173+}
Property changes on: trunk/extensions/MoodBar/tests/phpunit/ApiConcurrencyTest.php
___________________________________________________________________
Added: svn:eol-style
1174 + native
Index: trunk/extensions/MoodBar/tests/phpunit/ConcurrencyCheckTest.php
@@ -0,0 +1,104 @@
 2+<?php
 3+
 4+class ConcurrencyCheckTest extends MediaWikiTestCase {
 5+ /**
 6+ * @var Array of test users
 7+ */
 8+ public static $users;
 9+
 10+ // Prepare test environment
 11+
 12+ public function setUp() {
 13+ parent::setUp();
 14+
 15+ self::$users = array(
 16+ 'user1' => new ApiTestUser(
 17+ 'Concurrencychecktestuser1',
 18+ 'ConcurrencyCheck Test User 1',
 19+ 'concurrency_check_test_user_1@example.com',
 20+ array()
 21+ ),
 22+ 'user2' => new ApiTestUser(
 23+ 'Concurrencychecktestuser2',
 24+ 'ConcurrencyCheck Test User 2',
 25+ 'concurrency_check_test_user_2@example.com',
 26+ array()
 27+ ),
 28+ );
 29+
 30+ // turn on memcached for this test.
 31+ // if no memcached is present, this still works fine.
 32+ global $wgMainCacheType, $wgConcurrency;
 33+ $this->oldcache = $wgMainCacheType;
 34+ $wgMainCacheType = CACHE_MEMCACHED;
 35+ $wgConcurrency['ExpirationMin'] = -60; // negative numbers are needed for testing
 36+ }
 37+
 38+ public function tearDown() {
 39+ // turn off caching again.
 40+ global $wgMainCacheType;
 41+ $wgMainCacheType = $this->oldcache;
 42+
 43+ parent::tearDown();
 44+ }
 45+
 46+ // Actual tests from here down
 47+
 48+ public function testCheckoutCheckin() {
 49+ $first = new ConcurrencyCheck( 'CCUnitTest', self::$users['user1']->user );
 50+ $second = new ConcurrencyCheck( 'CCUnitTest', self::$users['user2']->user );
 51+ $testKey = 1337;
 52+
 53+ // clean up after any previously failed tests
 54+ $first->checkin($testKey);
 55+ $second->checkin($testKey);
 56+
 57+ // tests
 58+ $this->assertTrue( $first->checkout($testKey), "Initial checkout" );
 59+ $this->assertTrue( $first->checkout($testKey), "Cache hit" );
 60+ $this->assertFalse( $second->checkout($testKey), "Checkout of locked resource fails as different user" );
 61+ $this->assertTrue( $first->checkout($testKey), "Checkout of locked resource succeeds as original user" );
 62+ $this->assertFalse( $second->checkin($testKey), "Checkin of locked resource fails as different user" );
 63+ $this->assertTrue( $first->checkin($testKey), "Checkin of locked resource succeeds as original user" );
 64+ $second->setExpirationTime(-5);
 65+ $this->assertTrue( $second->checkout($testKey), "Checked-in resource is now available to second user" );
 66+ $second->setExpirationTime();
 67+ $this->assertTrue( $first->checkout($testKey), "Checkout of expired resource succeeds as first user");
 68+ $this->assertTrue( $second->checkout($testKey, true), "Checkout override" );
 69+ $this->assertFalse( $first->checkout($testKey), "Checkout of overriden resource fails as different user" );
 70+
 71+ // cleanup
 72+ $this->assertTrue( $second->checkin($testKey), "Checkin of record with changed ownership" );
 73+
 74+ }
 75+
 76+ public function testExpire() {
 77+ $cc = new ConcurrencyCheck( 'CCUnitTest', self::$users['user1']->user );
 78+ $cc->setExpirationTime(-1);
 79+ $cc->checkout( 1338 ); // these numbers are test record ids.
 80+ $cc->checkout( 1339 );
 81+ $cc->setExpirationTime();
 82+ $cc->checkout( 13310 );
 83+
 84+ // tests
 85+ $this->assertEquals( 2, $cc->expire(), "Resource expiration" );
 86+ $this->assertTrue( $cc->checkin( 13310 ), "Checkin succeeds after expiration" );
 87+ }
 88+
 89+ public function testStatus() {
 90+ $cc = new ConcurrencyCheck( 'CCUnitTest', self::$users['user1']->user );
 91+ $cc->checkout( 1337 );
 92+ $cc->checkout( 1338 );
 93+ $cc->setExpirationTime(-5);
 94+ $cc->checkout( 1339 );
 95+ $cc->setExpirationTime();
 96+
 97+ // tests
 98+ $output = $cc->status( array( 1337, 1338, 1339, 13310 ) );
 99+ $this->assertEquals( true, is_array( $output ), "Status returns values" );
 100+ $this->assertEquals( 4, count( $output ), "Output has the correct number of records" );
 101+ $this->assertEquals( 'valid', $output[1337]['status'], "Current checkouts are listed as valid");
 102+ $this->assertEquals( 'invalid', $output[1339]['status'], "Expired checkouts are invalid");
 103+ $this->assertEquals( 'invalid', $output[13310]['status'], "Missing checkouts are invalid");
 104+ }
 105+}
Property changes on: trunk/extensions/MoodBar/tests/phpunit/ConcurrencyCheckTest.php
___________________________________________________________________
Added: svn:eol-style
1106 + native
Index: trunk/extensions/MoodBar/MoodBar.php
@@ -23,6 +23,8 @@
2424 $wgAutoloadClasses['MoodBarHTMLEmailNotification'] = $moodBarDir . 'include/MoodBarHTMLEmailNotification.php';
2525 $wgAutoloadClasses['MoodBarHTMLMailerJob'] = $moodBarDir . 'include/MoodBarHTMLMailerJob.php';
2626 $wgAutoloadClasses['MoodBarUtil'] = $moodBarDir . 'include/MoodBarUtil.php';
 27+// commented out til bsitu can test this for me
 28+//$wgAutoloadClasses['ConcurrencyCheck'] = $moodBarDir . 'include/ConcurrencyCheck.php';
2729
2830 // API
2931 $wgAutoloadClasses['ApiMoodBar'] = $moodBarDir . 'ApiMoodBar.php';
@@ -36,6 +38,8 @@
3739 $wgAutoloadClasses['ApiMoodBarSetUserEmail'] = $moodBarDir . 'ApiMoodBarSetUserEmail.php';
3840 $wgAutoloadClasses['MWApiMoodBarSetUserEmailInvalidActionException'] = $moodBarDir . 'ApiMoodBarSetUserEmail.php';
3941 $wgAPIModules['moodbarsetuseremail'] = 'ApiMoodBarSetUserEmail';
 42+// commented out til bsitu can test this for me
 43+//$wgAutoloadClasses['ApiConcurrency'] = $moodBarDir . 'ApiConcurrency.php';
4044
4145 // Hooks
4246 $wgAutoloadClasses['MoodBarHooks'] = $moodBarDir . 'MoodBar.hooks.php';
Index: trunk/extensions/MoodBar/ApiConcurrency.php
@@ -0,0 +1,105 @@
 2+<?php
 3+
 4+/**
 5+ * API module that handles cooperative locking of web resources
 6+ */
 7+class ApiConcurrency extends ApiBase {
 8+ public function __construct( $main, $action ) {
 9+ parent::__construct( $main, $action );
 10+ }
 11+
 12+ public function execute() {
 13+ global $wgUser;
 14+
 15+ $this->checkPermission( $wgUser );
 16+
 17+ $params = $this->extractRequestParams();
 18+
 19+ $res = array();
 20+
 21+ $concurrencyCheck = new ConcurrencyCheck( $params['resourcetype'], $wgUser );
 22+
 23+ switch ( $params['ccaction'] ) {
 24+ case 'checkout':
 25+ case 'checkin':
 26+ if ( $concurrencyCheck->$params['ccaction']( $params['record'] ) ) {
 27+ $res['result'] = 'success';
 28+ } else {
 29+ $res['result'] = 'failure';
 30+ }
 31+ break;
 32+
 33+ default:
 34+ ApiBase::dieDebug( __METHOD__, "Unhandled concurrency action: {$params['ccaction']}" );
 35+ }
 36+
 37+ $this->getResult()->addValue( null, $this->getModuleName(), $res );
 38+ }
 39+
 40+ public function mustBePosted() {
 41+ return true;
 42+ }
 43+
 44+ public function isWriteMode() {
 45+ return true;
 46+ }
 47+
 48+ public function getAllowedParams() {
 49+ return array(
 50+ 'resourcetype' => array(
 51+ ApiBase::PARAM_TYPE => 'string',
 52+ ApiBase::PARAM_REQUIRED => true
 53+ ),
 54+ 'record' => array(
 55+ ApiBase::PARAM_TYPE => 'integer',
 56+ ApiBase::PARAM_REQUIRED => true
 57+ ),
 58+ 'token' => null,
 59+ 'expiry' => array(
 60+ ApiBase::PARAM_TYPE => 'integer'
 61+ ),
 62+ 'ccaction' => array(
 63+ ApiBase::PARAM_REQUIRED => true,
 64+ ApiBase::PARAM_TYPE => array(
 65+ 'checkout',
 66+ 'checkin',
 67+ ),
 68+ ),
 69+ );
 70+ }
 71+
 72+ public function getParamDescription() {
 73+ return array(
 74+ 'resourcetype' => 'the resource type for concurrency check',
 75+ 'record' => 'an unique identifier for a record of the defined resource type',
 76+ 'expiry' => 'the time interval for expiration',
 77+ 'ccaction' => 'the action for concurrency check',
 78+ );
 79+ }
 80+
 81+ public function getDescription() {
 82+ return 'Get/Set a concurrency check for a web resource type';
 83+ }
 84+
 85+ public function needsToken() {
 86+ return true;
 87+ }
 88+
 89+ public function getTokenSalt() {
 90+ return '';
 91+ }
 92+
 93+ public function getVersion() {
 94+ return __CLASS__ . ': $Id: ApiConcurrency.php 108560 2012-01-10 23:12:00Z reedy $';
 95+ }
 96+
 97+ private function checkPermission( $user ) {
 98+ if ( $user->isAnon() ) {
 99+ $this->dieUsage( "You don't have permission to do that", 'permission-denied' );
 100+ }
 101+ if ( $user->isBlocked( false ) ) {
 102+ $this->dieUsageMsg( array( 'blockedtext' ) );
 103+ }
 104+ }
 105+
 106+}
Property changes on: trunk/extensions/MoodBar/ApiConcurrency.php
___________________________________________________________________
Added: svn:eol-style
1107 + native

Past revisions this follows-up on

RevisionCommit summaryAuthorDate
r108559MERGE branches/concurrency 108301:108557 into trunkraindrift23:03, 10 January 2012
r108591sqlite needs its timestamps quoted...raindrift02:02, 11 January 2012

Status & tagging log