To participate you must create an account on apostrophenow.org. If you have already done so, click Login.

Changeset 4495

Show
Ignore:
Timestamp:
01/22/12 21:11:37 (4 months ago)
Author:
tboutell
Message:

Refactored the locking code. Since the mysql implementation didn't work (too much pooling of connections in PDO going on) and the flock implementation is still the default, there is little change in behavior for existing users.

You can now substitute your own implementation of the new aLock interface by setting app_a_lock_class and app_a_lock_options.

More exciting is the inclusion of aLockPkLockServer, an implementation that takes advantage of the new pkLockServer distributed lockserver:

 https://github.com/punkave/pkLockServer

While pkLockServer is brand spanking new, it stands up very well to some new features of apostrophenow.com that put a lot of stress on locking (which is how we found out the mysql implementation wasn't working for web requests, ouch).

Location:
plugins/apostrophePlugin/branches/1.5/lib
Files:
4 added
1 modified

Legend:

Unmodified
Added
Removed
  • plugins/apostrophePlugin/branches/1.5/lib/toolkit/BaseaTools.class.php

    r4278 r4495  
    11401140  } 
    11411141   
    1142   static protected $locks = array(); 
    1143  
    1144   /** 
    1145    * Lock names must be \w+ 
    1146    * @param mixed $name 
     1142  static protected $lockService = null; 
     1143 
     1144  /** 
     1145   * Lock names must be ascii alphanumeric and underscore characters 
     1146   * and must not be empty. If $wait is false, tries to get a lock 
     1147   * and returns false promptly if it is unable to do so right now, 
     1148   * true on success. If $wait is true, returns true on a successful 
     1149   * lock after waiting up to 30 seconds, otherwise throws an sfException. 
     1150   * 
     1151   * By default an implementation based on flock() is used, which is fine 
     1152   * for the vast majority of sites (see aLockFlock for details). If you 
     1153   * wish to use an alternative implementation, set app_a_lock_class to 
     1154   * the name of any implementation of our aLock interface, and set 
     1155   * app_a_lock_options to contain any necessary options to initialize it. 
     1156   *  
     1157   * @param string $name 
     1158   * @param bool $wait 
    11471159   */ 
    11481160  static public function lock($name, $wait = true) 
    11491161  { 
    1150     $lockType = sfConfig::get('app_a_lock_type', 'flock'); 
    1151     if ($lockType === 'mysql') 
    1152     { 
    1153       $info = aTools::getLockDbInfo(); 
    1154       $dbName = $info['dbName']; 
    1155       $sql = $info['sql']; 
    1156       $key = "$dbName-$name"; 
    1157       if ($wait) 
    1158       { 
    1159         $timeout = 30; 
    1160       } 
    1161       else 
    1162       { 
    1163         $timeout = 0; 
    1164       } 
    1165       $locked = $sql->queryOneScalar('SELECT GET_LOCK(:key, :timeout)', array('key' => $key, 'timeout' => $timeout)); 
    1166       if (!$locked) 
    1167       { 
    1168         if (!$wait) 
    1169         { 
    1170           return false; 
    1171         } 
    1172         else 
    1173         { 
    1174           throw new sfException("Unable to obtain a lock after 30 seconds. MySQL must be very busy."); 
    1175         } 
    1176       } 
    1177       aTools::$locks[] = $name; 
    1178       return true; 
    1179     } 
    1180     else 
    1181     { 
    1182       $dir = aFiles::getWritableDataFolder(array('a', 'locks')); 
    1183       if (!preg_match('/^\w+$/', $name)) 
    1184       { 
    1185         throw new sfException("Lock name is empty or contains non-word characters"); 
    1186       } 
    1187       $file = "$dir/$name.lck"; 
    1188       $tries = 0; 
    1189       while (true) 
    1190       { 
    1191         $fp = fopen($file, 'a'); 
    1192         if ($fp) 
    1193         { 
    1194           $flags = LOCK_EX; 
    1195           if (!$wait) 
    1196           { 
    1197             $flags |= LOCK_NB; 
    1198           } 
    1199           if (flock($fp, $flags)) 
    1200           { 
    1201             break; 
    1202           } 
    1203         } 
    1204         if (!$wait) 
    1205         { 
    1206           return false; 
    1207         } 
    1208         $tries++; 
    1209         if ($tries == 30) 
    1210         { 
    1211           throw new sfException("Unable to obtain a lock after 30 seconds. Set app_a_lockType to mysql or make sure $dir is on a filesystem that supports flock() calls."); 
    1212         } 
    1213         sleep(1); 
    1214       }  
    1215       aTools::$locks[] = $fp; 
    1216       return true; 
    1217     } 
    1218   } 
    1219  
    1220   static protected $lockSql; 
    1221   static protected $dbName; 
    1222    
    1223   /** 
    1224    * Connect to the lock database and return an array containing 
    1225    * dbName (the name of the *main* database for this site) and 
    1226    * sql (an aMysql object ready to talk to the *lock* database, which 
    1227    * can be separate). We need dbName to namespace our locks properly 
    1228    * 
    1229    * If app_a_lock_db is unspecified we use the default database 
    1230    * for locks too. IF THIS DATABASE IS A MASTER/SLAVE SETUP, LOCKS WILL NOT  
    1231    * BE PROPERLY MANAGED AND YOUR PAGE TREE WILL NOT BE SAFE. If this is a problem  
    1232    * for you, set up a separate mysql instance that is NOT master/slave as a lockserver  
    1233    * and specify the credentials for it in app_a_lock_db, giving the keys 
    1234    * host, user and password. No database name is required. 
    1235    * 
    1236    * Results are cached between calls during the same PHP request. Otherwise 
    1237    * we'd have separate connections to the lockdb and be unable to release what 
    1238    * we locked 
    1239    */ 
    1240     
    1241   static public function getLockDbInfo() 
    1242   { 
    1243     if (!isset(aTools::$lockSql)) 
    1244     { 
    1245       $mainSql = new aMysql(); 
    1246       $dbName = $mainSql->queryOneScalar('SELECT DATABASE()'); 
    1247       if (!strlen($dbName)) 
    1248       { 
    1249         throw new sfException('Unable to get name of main database for lock namespacing purposes'); 
    1250       } 
    1251       $params = sfConfig::get('app_a_lock_db'); 
    1252       if ($params) 
    1253       { 
    1254         $pdo = new PDO('mysql:host=' . $params['host'], $params['user'], $params['password']); 
    1255         if (!$pdo) 
    1256         { 
    1257           throw new sfException('Unable to connect to lock db specified by app_a_lock_db'); 
    1258         } 
    1259         $sql = new aMysql($pdo); 
    1260       } 
    1261       else 
    1262       { 
    1263         // OK, use the main database. This is NOT SAFE if you use MySQL replication  
    1264         $sql = new $mainSql; 
    1265       } 
    1266       aTools::$dbName = $dbName; 
    1267       aTools::$lockSql = $sql; 
    1268     } 
    1269     return array('dbName' => aTools::$dbName, 'sql' => aTools::$lockSql); 
    1270   } 
    1271  
    1272   /** 
    1273    * Release the most recent lock 
     1162    if (!aTools::$lockService) 
     1163    { 
     1164      $class = sfConfig::get('app_a_lock_class', 'aLockFlock'); 
     1165      $options = sfConfig::get('app_a_lock_options', array()); 
     1166      aTools::$lockService = new $class($options); 
     1167    } 
     1168    $result = aTools::$lockService->lock($name, $wait); 
     1169    if ($result) 
     1170    { 
     1171      error_log("Locked $name"); 
     1172    } 
     1173    return $result; 
     1174  } 
     1175 
     1176  /** 
     1177   * Release the most recent lock. It is not an error to call with no locks 
    12741178   */ 
    12751179  static public function unlock() 
    12761180  { 
    1277     $lockType = sfConfig::get('app_a_lock_type', 'flock'); 
    1278      
    1279     if ($lockType === 'mysql') 
    1280     { 
    1281       if (count(aTools::$locks)) 
    1282       { 
    1283         $name = array_pop(aTools::$locks); 
    1284         $info = aTools::getLockDbInfo(); 
    1285         $dbName = $info['dbName']; 
    1286         $sql = $info['sql']; 
    1287         $key = "$dbName-$name"; 
    1288         $sql->queryOneScalar('SELECT RELEASE_LOCK(:key)', array('key' => $key)); 
    1289       } 
    1290     } 
    1291     else 
    1292     { 
    1293       if (count(aTools::$locks)) 
    1294       { 
    1295         $fp = array_pop(aTools::$locks); 
    1296         fclose($fp); 
    1297       } 
    1298       else 
    1299       { 
    1300         // It's OK to call with no lock, this greatly simplifies methods like flunkUnless() 
    1301         // If you are using multiple names you are responsible for making sure you unlock consistently 
    1302       } 
    1303     } 
     1181    if (!isset(aTools::$lockService)) 
     1182    { 
     1183      /** 
     1184       * It is not an error to call with no locks 
     1185       */ 
     1186      return; 
     1187    } 
     1188    aTools::$lockService->unlock(); 
     1189    error_log("Unlocked"); 
    13041190  } 
    13051191