diff --git a/extensions/mutex/composer.json b/extensions/mutex/composer.json new file mode 100644 index 0000000..db8cde3 --- /dev/null +++ b/extensions/mutex/composer.json @@ -0,0 +1,27 @@ +{ + "name": "yiisoft/yii2-mutex", + "description": "Mutual exclusion extension for the Yii framework", + "keywords": ["yii", "mutex"], + "type": "library", + "license": "BSD-3-Clause", + "support": { + "issues": "https://github.com/yiisoft/yii2/issues?state=open", + "forum": "http://www.yiiframework.com/forum/", + "wiki": "http://www.yiiframework.com/wiki/", + "irc": "irc://irc.freenode.net/yii", + "source": "https://github.com/yiisoft/yii2" + }, + "authors": [ + { + "name": "resurtm", + "email": "resurtm@gmail.com" + } + ], + "minimum-stability": "dev", + "require": { + "yiisoft/yii2": "*" + }, + "autoload": { + "psr-0": { "yii\\mutex": "" } + } +} diff --git a/extensions/mutex/yii/mutex/DbMutex.php b/extensions/mutex/yii/mutex/DbMutex.php new file mode 100644 index 0000000..3699c36 --- /dev/null +++ b/extensions/mutex/yii/mutex/DbMutex.php @@ -0,0 +1,41 @@ + + * @since 2.0 + */ +abstract class DbMutex extends Mutex +{ + /** + * @var Connection|string the DB connection object or the application component ID of the DB connection. + * After the Mutex object is created, if you want to change this property, you should only assign + * it with a DB connection object. + */ + public $db = 'db'; + + /** + * Initializes generic database table based mutex implementation. + * @throws InvalidConfigException if [[db]] is invalid. + */ + public function init() + { + parent::init(); + if (is_string($this->db)) { + $this->db = Yii::$app->getComponent($this->db); + } + if (!$this->db instanceof Connection) { + throw new InvalidConfigException('Mutex::db must be either a DB connection instance or the application component ID of a DB connection.'); + } + } +} diff --git a/extensions/mutex/yii/mutex/FileMutex.php b/extensions/mutex/yii/mutex/FileMutex.php new file mode 100644 index 0000000..4a949d0 --- /dev/null +++ b/extensions/mutex/yii/mutex/FileMutex.php @@ -0,0 +1,86 @@ + + * @since 2.0 + */ +class FileMutex extends Mutex +{ + /** + * @var string the directory to store mutex files. You may use path alias here. + * If not set, it will use the "mutex" subdirectory under the application runtime path. + */ + public $mutexPath = '@runtime/mutex'; + /** + * @var resource[] stores all opened lock files. Keys are lock names and values are file handles. + */ + private $_files = array(); + + + /** + * Initializes mutex component implementation dedicated for UNIX, GNU/Linux, Mac OS X, and other UNIX-like + * operating systems. + * @throws InvalidConfigException + */ + public function init() + { + if (stripos(php_uname('s'), 'win') === 0) { + throw new InvalidConfigException('FileMutex does not have MS Windows operating system support.'); + } + $this->mutexPath = Yii::getAlias($this->mutexPath); + if (!is_dir($this->mutexPath)) { + mkdir($this->mutexPath, 0777, true); + } + } + + /** + * This method should be extended by concrete mutex implementations. Acquires lock by given name. + * @param string $name of the lock to be acquired. + * @param integer $timeout to wait for lock to become released. + * @return boolean acquiring result. + */ + protected function acquireLock($name, $timeout = 0) + { + $file = fopen($this->mutexPath . '/' . md5($name) . '.lock', 'w+'); + if ($file === false) { + return false; + } + $waitTime = 0; + while (!flock($file, LOCK_EX | LOCK_NB)) { + $waitTime++; + if ($waitTime > $timeout) { + fclose($file); + return false; + } + sleep(1); + } + $this->_files[$name] = $file; + return true; + } + + /** + * This method should be extended by concrete mutex implementations. Releases lock by given name. + * @param string $name of the lock to be released. + * @return boolean release result. + */ + protected function releaseLock($name) + { + if (!isset($this->_files[$name]) || !flock($this->_files[$name], LOCK_UN)) { + return false; + } else { + fclose($this->_files[$name]); + unset($this->_files[$name]); + return true; + } + } +} diff --git a/extensions/mutex/yii/mutex/Mutex.php b/extensions/mutex/yii/mutex/Mutex.php new file mode 100644 index 0000000..297abaf --- /dev/null +++ b/extensions/mutex/yii/mutex/Mutex.php @@ -0,0 +1,95 @@ + + * @since 2.0 + */ +abstract class Mutex extends Component +{ + /** + * @var boolean whether all locks acquired in this process (i.e. local locks) must be released automagically + * before finishing script execution. Defaults to true. Setting this property to true means that all locks + * acquire in this process must be released in any case (regardless any kind of errors or exceptions). + */ + public $autoRelease = true; + /** + * @var string[] names of the locks acquired in the current PHP process. + */ + private $_locks = array(); + + + /** + * Initializes the mutex component. + */ + public function init() + { + if ($this->autoRelease) { + $mutex = $this; + $locks = &$this->_locks; + register_shutdown_function(function () use ($mutex, &$locks) { + foreach ($locks as $lock) { + $mutex->release($lock); + } + }); + } + } + + /** + * @param string $name of the lock to be acquired. Must be unique. + * @param integer $timeout to wait for lock to be released. Defaults to zero meaning that method will return + * false immediately in case lock was already acquired. + * @return boolean lock acquiring result. + */ + public function acquire($name, $timeout = 0) + { + if ($this->acquireLock($name, $timeout)) { + $this->_locks[] = $name; + return true; + } else { + return false; + } + } + + /** + * Release acquired lock. This method will return false in case named lock was not found. + * @param string $name of the lock to be released. This lock must be already created. + * @return boolean lock release result: false in case named lock was not found.. + */ + public function release($name) + { + if ($this->releaseLock($name)) { + $index = array_search($name, $this->_locks); + if ($index !== false) { + unset($this->_locks[$index]); + } + return true; + } else { + return false; + } + } + + /** + * This method should be extended by concrete mutex implementations. Acquires lock by given name. + * @param string $name of the lock to be acquired. + * @param integer $timeout to wait for lock to become released. + * @return boolean acquiring result. + */ + abstract protected function acquireLock($name, $timeout = 0); + + /** + * This method should be extended by concrete mutex implementations. Releases lock by given name. + * @param string $name of the lock to be released. + * @return boolean release result. + */ + abstract protected function releaseLock($name); +} diff --git a/extensions/mutex/yii/mutex/MysqlMutex.php b/extensions/mutex/yii/mutex/MysqlMutex.php new file mode 100644 index 0000000..b04427a --- /dev/null +++ b/extensions/mutex/yii/mutex/MysqlMutex.php @@ -0,0 +1,57 @@ + + * @since 2.0 + */ +class MysqlMutex extends Mutex +{ + /** + * Initializes MySQL specific mutex component implementation. + * @throws InvalidConfigException if [[db]] is not MySQL connection. + */ + public function init() + { + parent::init(); + if ($this->db->driverName !== 'mysql') { + throw new InvalidConfigException('In order to use MysqlMutex connection must be configured to use MySQL database.'); + } + } + + /** + * This method should be extended by concrete mutex implementations. Acquires lock by given name. + * @param string $name of the lock to be acquired. + * @param integer $timeout to wait for lock to become released. + * @return boolean acquiring result. + * @see http://dev.mysql.com/doc/refman/5.0/en/miscellaneous-functions.html#function_get-lock + */ + protected function acquireLock($name, $timeout = 0) + { + return (boolean)$this->db + ->createCommand('SELECT GET_LOCK(:name, :timeout)', array(':name' => $name, ':timeout' => $timeout)) + ->queryScalar(); + } + + /** + * This method should be extended by concrete mutex implementations. Releases lock by given name. + * @param string $name of the lock to be released. + * @return boolean release result. + * @see http://dev.mysql.com/doc/refman/5.0/en/miscellaneous-functions.html#function_release-lock + */ + protected function releaseLock($name) + { + return (boolean)$this->db + ->createCommand('SELECT RELEASE_LOCK(:name)', array(':name' => $name)) + ->queryScalar(); + } +}