r108863 MediaWiki - Code Review archive

Repository:MediaWiki
Revision:r108862‎ | r108863 | r108864 >
Date:22:10, 13 January 2012
Author:raindrift
Status:ok (Comments)
Tags:
Comment:
moved ConcurrencyCheck code into its own extension
Modified paths:
  • /trunk/extensions/InterfaceConcurrency (added) (history)
  • /trunk/extensions/InterfaceConcurrency/ApiConcurrency.php (added) (history)
  • /trunk/extensions/InterfaceConcurrency/InterfaceConcurrency.hooks.php (added) (history)
  • /trunk/extensions/InterfaceConcurrency/InterfaceConcurrency.i18n.php (added) (history)
  • /trunk/extensions/InterfaceConcurrency/InterfaceConcurrency.php (added) (history)
  • /trunk/extensions/InterfaceConcurrency/includes (added) (history)
  • /trunk/extensions/InterfaceConcurrency/includes/ConcurrencyCheck.php (added) (history)
  • /trunk/extensions/InterfaceConcurrency/sql (added) (history)
  • /trunk/extensions/InterfaceConcurrency/sql/concurrencycheck.sql (added) (history)
  • /trunk/extensions/InterfaceConcurrency/tests (added) (history)
  • /trunk/extensions/InterfaceConcurrency/tests/phpunit (added) (history)
  • /trunk/extensions/MoodBar/ApiConcurrency.php (deleted) (history)
  • /trunk/extensions/MoodBar/MoodBar.php (modified) (history)
  • /trunk/extensions/MoodBar/include/ConcurrencyCheck.php (deleted) (history)
  • /trunk/extensions/MoodBar/sql/concurrencycheck.sql (deleted) (history)
  • /trunk/extensions/MoodBar/tests/phpunit (deleted) (history)

Diff [purge]

Index: trunk/extensions/InterfaceConcurrency/InterfaceConcurrency.i18n.php
@@ -0,0 +1,20 @@
 2+<?php
 3+/**
 4+ * Internationalisation file for extension InterfaceConcurrency
 5+ *
 6+ * @file
 7+ * @ingroup Extensions
 8+ */
 9+
 10+$messages = array();
 11+
 12+/** English
 13+ * @author Ian Baker
 14+ */
 15+$messages['en'] = array(
 16+ 'interfaceconcurrency-desc' => 'Provides backend for UI that shows which resources are in use',
 17+);
 18+
 19+$messages['qqq'] = array(
 20+ 'interfaceconcurrency-desc' => 'Description of this extension',
 21+);
Property changes on: trunk/extensions/InterfaceConcurrency/InterfaceConcurrency.i18n.php
___________________________________________________________________
Added: svn:eol-style
122 + native
Index: trunk/extensions/InterfaceConcurrency/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/InterfaceConcurrency/sql/concurrencycheck.sql
___________________________________________________________________
Added: svn:eol-style
127 + native
Index: trunk/extensions/InterfaceConcurrency/InterfaceConcurrency.php
@@ -0,0 +1,60 @@
 2+<?php
 3+/**
 4+ * InterfaceConcurrency extension
 5+ *
 6+ * @file
 7+ * @ingroup Extensions
 8+ *
 9+ * Backend for cooperative locking of web resources
 10+ *
 11+ * Each resource is identified by a combination of the "resource type" (the application, the type
 12+ * of content, etc), and the resource's primary key or some other unique numeric ID.
 13+ *
 14+ * Primarily, this extension provides an API for accessing the functionality. It should probably be
 15+ * rolled into core after 1.19 is branched.
 16+ *
 17+ * Usage: Add the following line in LocalSettings.php:
 18+ * require_once( "$IP/extensions/InterfaceConcurrency/InterfaceConcurrency.php" );
 19+ *
 20+ * @author Ian Baker <ian@wikimedia.org>, Benny Situ <bsitu@wikimedia.org>
 21+ * @license GPL v2
 22+ * @version 0.1.0
 23+ */
 24+
 25+// Check environment
 26+if ( !defined( 'MEDIAWIKI' ) ) {
 27+ echo( "This is an extension to MediaWiki and cannot be run standalone.\n" );
 28+ die( - 1 );
 29+}
 30+
 31+// Credits
 32+$wgExtensionCredits['other'][] = array(
 33+ 'path' => __FILE__,
 34+ 'name' => 'InterfaceConcurrency',
 35+ 'author' => array( 'Ian Baker', 'Benny Situ' ),
 36+ 'version' => '0.1.0',
 37+ 'url' => 'https://www.mediawiki.org/wiki/Extension:InterfaceConcurrency',
 38+ 'descriptionmsg' => 'interfaceconcurrency-desc',
 39+);
 40+
 41+// Shortcut to this extension directory
 42+$dir = dirname( __FILE__ ) . '/';
 43+
 44+// Object model
 45+$wgExtensionMessagesFiles['InterfaceConcurrency'] = $dir . 'InterfaceConcurrency.i18n.php';
 46+$wgAutoloadClasses['ConcurrencyCheck'] = $dir . 'includes/ConcurrencyCheck.php';
 47+
 48+// API
 49+$wgAutoloadClasses['ApiConcurrency'] = $dir . 'ApiConcurrency.php';
 50+$wgAPIModules['concurrency'] = 'ApiConcurrency';
 51+
 52+// Hooks
 53+$wgHooks['LoadExtensionSchemaUpdates'][] = 'InterfaceConcurrencyHooks::onLoadExtensionSchemaUpdates';
 54+
 55+// Configuration
 56+$wgConcurrency = array(
 57+ 'ExpirationDefault' => 60 * 15, // Default checkout duration. 15 minutes.
 58+ 'ExpirationMax' => 60 * 30, // Maximum possible checkout duration. 30 minutes.
 59+ 'ExpirationMin' => 1, // Minimum possible checkout duration. Negative is possible for testing if you want.
 60+ 'TrustMemc' => true // If running in an environment with multiple discrete caches, set to false.
 61+);
Property changes on: trunk/extensions/InterfaceConcurrency/InterfaceConcurrency.php
___________________________________________________________________
Added: svn:eol-style
162 + native
Index: trunk/extensions/InterfaceConcurrency/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/InterfaceConcurrency/tests/phpunit/ApiConcurrencyTest.php
___________________________________________________________________
Added: svn:eol-style
1174 + native
Index: trunk/extensions/InterfaceConcurrency/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/InterfaceConcurrency/tests/phpunit/ConcurrencyCheckTest.php
___________________________________________________________________
Added: svn:eol-style
1106 + native
Index: trunk/extensions/InterfaceConcurrency/InterfaceConcurrency.hooks.php
@@ -0,0 +1,18 @@
 2+<?php
 3+
 4+class InterfaceConcurrencyHooks {
 5+
 6+ /**
 7+ * Runs InterfaceConcurrency schema updates#
 8+ *
 9+ * @param $updater DatabaseUpdater
 10+ */
 11+ public static function onLoadExtensionSchemaUpdates( $updater = null ) {
 12+ $dir = dirname(__FILE__) . '/sql';
 13+
 14+ $updater->addExtensionTable( 'concurrencycheck', "$dir/concurrencycheck.sql" );
 15+
 16+ return true;
 17+ }
 18+
 19+}
\ No newline at end of file
Property changes on: trunk/extensions/InterfaceConcurrency/InterfaceConcurrency.hooks.php
___________________________________________________________________
Added: svn:eol-style
120 + native
Index: trunk/extensions/InterfaceConcurrency/includes/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/InterfaceConcurrency/includes/ConcurrencyCheck.php
___________________________________________________________________
Added: svn:eol-style
1360 + native
Index: trunk/extensions/InterfaceConcurrency/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/InterfaceConcurrency/ApiConcurrency.php
___________________________________________________________________
Added: svn:eol-style
1107 + native
Index: trunk/extensions/MoodBar/ApiConcurrency.php
@@ -1,105 +0,0 @@
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 -}
Index: trunk/extensions/MoodBar/include/ConcurrencyCheck.php
@@ -1,358 +0,0 @@
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 {}
Index: trunk/extensions/MoodBar/sql/concurrencycheck.sql
@@ -1,25 +0,0 @@
2 -CREATE TABLE /*_*/concurrencycheck (
3 - -- a string describing the resource or application being checked out.
4 - cc_resource_type varchar(255) NOT NULL,
5 -
6 - -- the (numeric) ID of the record that's being checked out.
7 - cc_record int unsigned NOT NULL,
8 -
9 - -- the user who has control of the resource
10 - cc_user int unsigned NOT NULL,
11 -
12 - -- the date/time on which this record expires
13 - cc_expiration varbinary(14) not null
14 -
15 -) /*$wgDBTableOptions*/;
16 -CREATE UNIQUE INDEX /*i*/cc_resource_record ON /*_*/concurrencycheck (cc_resource_type, cc_record);
17 -CREATE INDEX /*i*/cc_user ON /*_*/concurrencycheck (cc_user);
18 -CREATE INDEX /*i*/cc_expiration ON /*_*/concurrencycheck (cc_expiration);
Index: trunk/extensions/MoodBar/MoodBar.php
@@ -23,8 +23,6 @@
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';
2927
3028 // API
3129 $wgAutoloadClasses['ApiMoodBar'] = $moodBarDir . 'ApiMoodBar.php';
@@ -38,8 +36,6 @@
3937 $wgAutoloadClasses['ApiMoodBarSetUserEmail'] = $moodBarDir . 'ApiMoodBarSetUserEmail.php';
4038 $wgAutoloadClasses['MWApiMoodBarSetUserEmailInvalidActionException'] = $moodBarDir . 'ApiMoodBarSetUserEmail.php';
4139 $wgAPIModules['moodbarsetuseremail'] = 'ApiMoodBarSetUserEmail';
42 -// commented out til bsitu can test this for me
43 -//$wgAutoloadClasses['ApiConcurrency'] = $moodBarDir . 'ApiConcurrency.php';
4440
4541 // Hooks
4642 $wgAutoloadClasses['MoodBarHooks'] = $moodBarDir . 'MoodBar.hooks.php';

Follow-up revisions

RevisionCommit summaryAuthorDate
r109066r108863: Register extension for translatewiki.net.raymond21:47, 16 January 2012

Comments

#Comment by Siebrand (talk | contribs)   22:14, 13 January 2012

Status & tagging log