CRON: protection against re-entrance now relies on a bullet-proof mutex. Also added the option 'debug=1' to output the call stack in case an exception occurs (not always because of passwords being shown in the call stack)

SVN:trunk[2834]
This commit is contained in:
Romain Quetiez
2013-08-26 15:29:32 +00:00
parent f8c3e0ddea
commit abae2129ad
2 changed files with 159 additions and 27 deletions

138
core/mutex.class.inc.php Normal file
View File

@@ -0,0 +1,138 @@
<?php
// Copyright (C) 2013 Combodo SARL
//
// This file is part of iTop.
//
// iTop is free software; you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// iTop is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with iTop. If not, see <http://www.gnu.org/licenses/>
/**
* Class iTopMutex
* A class to serialize the execution of some code sections
* Emulates the API of PECL Mutex class
* Relies on MySQL locks because the API sem_get is not always present in the
* installed PHP.
*
* @copyright Copyright (C) 2013 Combodo SARL
* @license http://opensource.org/licenses/AGPL-3.0
*/
class iTopMutex
{
protected $sName;
protected $hDBLink;
public function __construct($sName)
{
// Compute the name of a lock for mysql
// Note: the name is server-wide!!!
$this->sName = 'itop.'.$sName;
// It is a MUST to create a dedicated session each time a lock is required, because
// using GET_LOCK anytime on the same session will RELEASE the current and unique session lock (known issue)
$oConfig = utils::GetConfig();
$this->InitMySQLSession($oConfig->GetDBHost(), $oConfig->GetDBUser(), $oConfig->GetDBPwd());
}
public function __destruct()
{
$this->Unlock();
mysqli_close($this->hDBLink);
}
/**
* Acquire the mutex
*/
public function Lock()
{
do
{
$res = $this->QueryToScalar("SELECT GET_LOCK('".$this->sName."', 3600)");
if (is_null($res))
{
throw new Exception("Failed to acquire the lock '".$this->sName."'");
}
// $res === '1' means I hold the lock
// $res === '0' means it timed out
}
while ($res !== '1');
}
/**
* Attempt to acquire the mutex
* @returns bool True if the mutex is acquired, false if already locked elsewhere
*/
public function TryLock()
{
$res = $this->QueryToScalar("SELECT GET_LOCK('".$this->sName."', 0)");
if (is_null($res))
{
throw new Exception("Failed to acquire the lock '".$this->sName."'");
}
// $res === '1' means I hold the lock
// $res === '0' means it timed out
return ($res === '1');
}
/**
* Release the mutex
*/
public function Unlock()
{
$res = $this->QueryToScalar("SELECT RELEASE_LOCK('".$this->sName."')");
}
public function InitMySQLSession($sHost, $sUser, $sPwd)
{
$aConnectInfo = explode(':', $sHost);
if (count($aConnectInfo) > 1)
{
// Override the default port
$sServer = $aConnectInfo[0];
$iPort = $aConnectInfo[1];
$this->hDBLink = @mysqli_connect($sServer, $sUser, $sPwd, '', $iPort);
}
else
{
$this->hDBLink = @mysqli_connect($sHost, $sUser, $sPwd);
}
if (!$this->hDBLink)
{
throw new Exception("Could not connect to the DB server (host=$sHost, user=$sUser): ".mysqli_connect_error().' (mysql errno: '.mysqli_connect_errno().')');
}
}
protected function QueryToScalar($sSql)
{
$result = mysqli_query($this->hDBLink, $sSql);
if (!$result)
{
throw new Exception("Failed to issue MySQL query '".$sSql."': ".mysqli_error($this->hDBLink).' (mysql errno: '.mysqli_errno($this->hDBLink).')');
}
if ($aRow = mysqli_fetch_array($result, MYSQLI_BOTH))
{
$res = $aRow[0];
}
else
{
mysqli_free_result($result);
throw new Exception("No result for query '".$sSql."'");
}
mysqli_free_result($result);
return $res;
}
}

View File

@@ -19,7 +19,7 @@
/**
* Heart beat of the application (process asynchron tasks such as broadcasting email)
*
* @copyright Copyright (C) 2010-2012 Combodo SARL
* @copyright Copyright (C) 2010-2013 Combodo SARL
* @license http://opensource.org/licenses/AGPL-3.0
*/
@@ -59,7 +59,7 @@ function UsageAndExit($oP)
if ($bModeCLI)
{
$oP->p("USAGE:\n");
$oP->p("php cron.php --auth_user=<login> --auth_pwd=<password> [--param_file=<file>] [--verbose=1] [--status_only=1]\n");
$oP->p("php cron.php --auth_user=<login> --auth_pwd=<password> [--param_file=<file>] [--verbose=1] [--debug=1] [--status_only=1]\n");
}
else
{
@@ -337,6 +337,7 @@ foreach(get_declared_classes() as $sPHPClass)
$bVerbose = utils::ReadParam('verbose', false, true /* Allow CLI */);
$bDebug = utils::ReadParam('debug', false, true /* Allow CLI */);
if ($bVerbose)
{
@@ -355,42 +356,35 @@ if (utils::ReadParam('status_only', false, true /* Allow CLI */))
exit(0);
}
// Compute the name of a lock for mysql
// The name is server-wide
$oConfig = utils::GetConfig();
$sLockName = 'itop.cron.'.$oConfig->GetDBName().'_'.$oConfig->GetDBSubname();
require_once(APPROOT.'core/mutex.class.inc.php');
$oP->p("Starting: ".time().' ('.date('Y-m-d H:i:s').')');
// CAUTION: using GET_LOCK anytime on the same connexion will RELEASE the lock
// Todo: invoke GET_LOCK from a dedicated session (encapsulate that into a mutex class)
$res = CMDBSource::QueryToScalar("SELECT GET_LOCK('$sLockName', 1)");// timeout = 1 second (see also IS_FREE_LOCK)
if (is_null($res))
try
{
// TODO - Log ?
$oP->p("ERROR: Failed to acquire the lock '$sLockName'");
}
elseif ($res === '1')
{
// The current session holds the lock
try
$oConfig = utils::GetConfig();
$oMutex = new iTopMutex('cron.'.$oConfig->GetDBName().'_'.$oConfig->GetDBSubname());
if ($oMutex->TryLock())
{
CronExec($oP, $aProcesses, $bVerbose);
$oMutex->Unlock();
}
catch(Exception $e)
else
{
// TODO - Log ?
$oP->p("ERROR:".$e->getMessage());
$oP->p($e->getTraceAsString());
// Exit silently
$oP->p("Already running...");
}
$res = CMDBSource::QueryToScalar("SELECT RELEASE_LOCK('$sLockName')");
}
else
catch (Exception $e)
{
// Lock already held by another session
// Exit silently
$oP->p("Already running...");
$oP->p("ERROR: '".$e->getMessage()."'");
if ($bDebug)
{
// Might contain verb parameters such a password...
$oP->p($e->getTraceAsString());
}
}
$oP->p("Exiting: ".time().' ('.date('Y-m-d H:i:s').')');
$oP->Output();