* @author Ian Baker <ian@wikimedia.org>
*/
class ConcurrencyCheck {
+
+ protected $expirationTime;
+
+ /**
+ * @var User
+ */
+ protected $user;
+
/**
* Constructor
*
'cc_expiration' => time() + $this->expirationTime,
),
__METHOD__,
- array('IGNORE')
+ array( 'IGNORE' )
);
// if the insert succeeded, checkout is done.
$dbw = $this->dbw;
$userId = $this->user->getId();
$cacheKey = wfMemcKey( $this->resourceType, $record );
-
+
$dbw->delete(
'concurrencycheck',
array(
__METHOD__,
array()
);
-
+
// check row count (this is atomic, select would not be)
if( $dbw->affectedRows() === 1 ) {
$memc->delete( $cacheKey );
return true;
}
-
- return false;
+
+ return false;
}
/**
*/
public function expire() {
$memc = wfGetMainCache();
- $dbw = $this->dbw;
+ $dbw = $this->dbw;
$now = time();
-
+
// get the rows to remove from cache.
$res = $dbw->select(
'concurrencycheck',
__METHOD__,
array()
);
-
+
// build a list of rows to delete.
$toExpire = array();
while( $res && $record = $res->fetchRow() ) {
$toExpire[] = $record['cc_record'];
}
-
+
// remove the rows from the db
$dbw->delete(
'concurrencycheck',
__METHOD__,
array()
);
-
+
// delete all those rows from cache
// outside a transaction because deletes don't require atomicity.
foreach( $toExpire as $expire ) {
// return the number of rows removed.
return $dbw->affectedRows();
}
-
+
public function status( $keys ) {
$memc = wfGetMainCache();
$dbw = $this->dbw;
$checkouts = array();
$toSelect = array();
-
+
// validate keys, attempt to retrieve from cache.
foreach( $keys as $key ) {
$this->validateId( $key );
-
+
$cached = $memc->get( wfMemcKey( $this->resourceType, $key ) );
if( $cached && $cached['expiration'] > $now ) {
$checkouts[$key] = array(
__METHOD__,
array()
);
-
+
while( $res && $record = $res->fetchRow() ) {
$record['status'] = 'valid';
$checkouts[ $record['cc_record'] ] = $record;
-
+
// safe to store values since this is inside the transaction
$memc->set(
wfMemcKey( $this->resourceType, $record['cc_record'] ),
// end the transaction.
$dbw->rollback();
}
-
+
// if a key was passed in but has no (unexpired) checkout, include it in the
// result set to make things easier and more consistent on the client-side.
foreach( $keys as $key ) {
$checkouts[$key]['status'] = 'invalid';
}
}
-
+
return $checkouts;
}
-
+
public function listCheckouts() {
// TODO: fill in the function that lets you get the complete set of checkouts for a given application.
}
-
- public function setUser ( $user ) {
+
+ /**
+ * @param $user user
+ */
+ public function setUser( $user ) {
$this->user = $user;
}
-
- public function setExpirationTime ( $expirationTime = null ) {
+
+ public function setExpirationTime( $expirationTime = null ) {
global $wgConcurrencyExpirationDefault, $wgConcurrencyExpirationMax, $wgConcurrencyExpirationMin;
// check to make sure the time is digits only, so it can be used in queries
if( ! preg_match('/^\d+$/', $record) ) {
throw new ConcurrencyCheckBadRecordIdException( 'Record ID ' . $record . ' must be a positive integer' );
}
-
+
// TODO: add a hook here for client-side validation.
return true;
}
}
-class ConcurrencyCheckBadRecordIdException extends MWException {};
+class ConcurrencyCheckBadRecordIdException extends MWException {}
* API module that handles cooperative locking of web resources
*/
class ApiConcurrency extends ApiBase {
-
public function __construct( $main, $action ) {
parent::__construct( $main, $action );
}
case 'checkout':
case 'checkin':
if ( $concurrencyCheck->$params['ccaction']( $params['record'] ) ) {
- $res['result'] = 'success';
- }
- else {
- $res['result'] = 'failure';
+ $res['result'] = 'success';
+ } else {
+ $res['result'] = 'failure';
}
break;
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiConcurrency.php $';
+ return __CLASS__ . ': $Id$';
}
private function checkPermission( $user ) {
}
}
-}
\ No newline at end of file
+}
* @var Array of test users
*/
public static $users;
-
+
// Prepare test environment
-
+
public function setUp() {
parent::setUp();
array()
),
);
-
+
// turn on memcached for this test.
// if no memcached is present, this still works fine.
global $wgMainCacheType;
$this->oldcache = $wgMainCacheType;
$wgMainCacheType = CACHE_MEMCACHED;
}
-
+
public function tearDown() {
// turn off caching again.
global $wgMainCacheType;
$wgMainCacheType = $this->oldcache;
-
- parent::tearDown();
+
+ parent::tearDown();
}
-
+
// Actual tests from here down
public function testCheckoutCheckin() {
// clean up after any previously failed tests
$first->checkin($testKey);
$second->checkin($testKey);
-
+
// tests
$this->assertTrue( $first->checkout($testKey), "Initial checkout" );
$this->assertTrue( $first->checkout($testKey), "Cache hit" );
$cc->checkout( 1339 );
$cc->setExpirationTime();
$cc->checkout( 13310 );
-
+
// tests
$this->assertEquals( 2, $cc->expire(), "Resource expiration" );
$this->assertTrue( $cc->checkin( 13310 ), "Checkin succeeds after expiration" );
$this->assertEquals( 'invalid', $output[1339]['status'], "Expired checkouts are invalid");
$this->assertEquals( 'invalid', $output[13310]['status'], "Missing checkouts are invalid");
}
-}
\ No newline at end of file
+}