Merge remote-tracking branch 'origin/support/2.7' into develop

# Conflicts:
#	core/cmdbsource.class.inc.php
#	core/coreexception.class.inc.php
#	core/log.class.inc.php
This commit is contained in:
Pierre Goiffon
2021-06-21 16:24:27 +02:00
10 changed files with 193 additions and 104 deletions

View File

@@ -28,14 +28,14 @@ class CoreCannotSaveObjectException extends CoreException
*
* @param array $aContextData containing at least those keys : issues, id, class
*/
public function __construct($aContextData)
public function __construct($aContextData, $oPrevious = null)
{
$this->aIssues = $aContextData['issues'];
$this->iObjectId = $aContextData['id'];
$this->sObjectClass = $aContextData['class'];
$sIssues = implode(', ', $this->aIssues);
parent::__construct($sIssues, $aContextData);
parent::__construct($sIssues, $aContextData, '', $oPrevious);
}
/**

View File

@@ -585,6 +585,11 @@ class CMDBSource
*/
private static function DBQuery($sSql)
{
$sShortSQL = str_replace("\n", " ", substr($sSql, 0, 120));
if (substr_compare($sShortSQL, "SELECT", 0, strlen("SELECT")) !== 0) {
IssueLog::Trace("$sShortSQL", 'cmdbsource');
}
$oKPI = new ExecutionKPI();
try
{
@@ -668,10 +673,15 @@ class CMDBSource
*/
private static function StartTransaction()
{
$aStackTrace = debug_backtrace(DEBUG_BACKTRACE_PROVIDE_OBJECT , 3);
$sCaller = 'From '.$aStackTrace[1]['file'].'('.$aStackTrace[1]['line'].'): '.$aStackTrace[2]['class'].'->'.$aStackTrace[2]['function'].'()';
$bHasExistingTransactions = self::IsInsideTransaction();
if (!$bHasExistingTransactions)
{
IssueLog::Trace("START TRANSACTION $sCaller", 'cmdbsource');
self::DBQuery('START TRANSACTION');
} else {
IssueLog::Trace("Ignore nested (".self::$m_iTransactionLevel.") START TRANSACTION $sCaller", 'cmdbsource');
}
self::AddTransactionLevel();
@@ -689,9 +699,12 @@ class CMDBSource
*/
private static function Commit()
{
$aStackTrace = debug_backtrace(DEBUG_BACKTRACE_PROVIDE_OBJECT , 3);
$sCaller = 'From '.$aStackTrace[1]['file'].'('.$aStackTrace[1]['line'].'): '.$aStackTrace[2]['class'].'->'.$aStackTrace[2]['function'].'()';
if (!self::IsInsideTransaction())
{
// should not happen !
IssueLog::Error("No Transaction COMMIT $sCaller", 'cmdbsource');
throw new MySQLNoTransactionException('Trying to commit transaction whereas none have been started !', null);
}
@@ -699,8 +712,10 @@ class CMDBSource
if (self::IsInsideTransaction())
{
IssueLog::Trace("Ignore nested (".self::$m_iTransactionLevel.") COMMIT $sCaller", 'cmdbsource');
return;
}
IssueLog::Trace("COMMIT $sCaller", 'cmdbsource');
self::DBQuery('COMMIT');
}
@@ -719,17 +734,22 @@ class CMDBSource
*/
private static function Rollback()
{
$aStackTrace = debug_backtrace(DEBUG_BACKTRACE_PROVIDE_OBJECT , 3);
$sCaller = 'From '.$aStackTrace[1]['file'].'('.$aStackTrace[1]['line'].'): '.$aStackTrace[2]['class'].'->'.$aStackTrace[2]['function'].'()';
if (!self::IsInsideTransaction())
{
// should not happen !
IssueLog::Error("No Transaction ROLLBACK $sCaller", 'cmdbsource');
throw new MySQLNoTransactionException('Trying to commit transaction whereas none have been started !', null);
}
self::RemoveLastTransactionLevel();
if (self::IsInsideTransaction())
{
IssueLog::Trace("Ignore nested (".self::$m_iTransactionLevel.") ROLLBACK $sCaller", 'cmdbsource');
return;
}
IssueLog::Trace("ROLLBACK $sCaller", 'cmdbsource');
self::DBQuery('ROLLBACK');
}
@@ -779,6 +799,18 @@ class CMDBSource
self::$m_iTransactionLevel = 0;
}
public static function IsDeadlockException(Exception $e)
{
while ($e instanceof Exception) {
if (($e instanceof MySQLException) && ($e->getCode() == 1213)) {
return true;
}
$e = $e->getPrevious();
}
return false;
}
public static function GetInsertId()
{
$iRes = self::$m_oMysqli->insert_id;

View File

@@ -2778,50 +2778,72 @@ abstract class DBObject implements iDisplay
}
}
$iTransactionRetry = 1;
$bIsTransactionEnabled = MetaModel::GetConfig()->Get('db_core_transactions_enabled');
try
if ($bIsTransactionEnabled)
{
if ($bIsTransactionEnabled)
{
CMDBSource::Query('START TRANSACTION');
}
// First query built upon on the root class, because the ID must be created first
$this->m_iKey = $this->DBInsertSingleTable($sRootClass);
// Then do the leaf class, if different from the root class
if ($sClass != $sRootClass)
{
$this->DBInsertSingleTable($sClass);
}
// Then do the other classes
foreach (MetaModel::EnumParentClasses($sClass) as $sParentClass)
{
if ($sParentClass == $sRootClass)
{
continue;
}
$this->DBInsertSingleTable($sParentClass);
}
$this->OnObjectKeyReady();
$this->DBWriteLinks();
$this->WriteExternalAttributes();
if ($bIsTransactionEnabled)
{
CMDBSource::Query('COMMIT');
}
// TODO Deep clone this object before the transaction (to use it in case of rollback)
// $iTransactionRetryCount = MetaModel::GetConfig()->Get('db_core_transactions_retry_count');
$iTransactionRetryCount = 1;
$iTransactionRetryDelay = MetaModel::GetConfig()->Get('db_core_transactions_retry_delay_ms');
$iTransactionRetry = $iTransactionRetryCount;
}
catch (Exception $e)
{
if ($bIsTransactionEnabled)
{
CMDBSource::Query('ROLLBACK');
while ($iTransactionRetry > 0) {
try {
$iTransactionRetry--;
if ($bIsTransactionEnabled) {
CMDBSource::Query('START TRANSACTION');
}
// First query built upon on the root class, because the ID must be created first
$this->m_iKey = $this->DBInsertSingleTable($sRootClass);
// Then do the leaf class, if different from the root class
if ($sClass != $sRootClass) {
$this->DBInsertSingleTable($sClass);
}
// Then do the other classes
foreach (MetaModel::EnumParentClasses($sClass) as $sParentClass) {
if ($sParentClass == $sRootClass) {
continue;
}
$this->DBInsertSingleTable($sParentClass);
}
$this->OnObjectKeyReady();
$this->DBWriteLinks();
$this->WriteExternalAttributes();
if ($bIsTransactionEnabled) {
CMDBSource::Query('COMMIT');
}
break;
}
catch (Exception $e) {
IssueLog::Error($e->getMessage());
if ($bIsTransactionEnabled)
{
CMDBSource::Query('ROLLBACK');
if (!CMDBSource::IsInsideTransaction() && CMDBSource::IsDeadlockException($e))
{
// Deadlock found when trying to get lock; try restarting transaction (only in main transaction)
if ($iTransactionRetry > 0)
{
// wait and retry
IssueLog::Error("Insert TRANSACTION Retrying...");
usleep(random_int(1, 5) * 1000 * $iTransactionRetryDelay * ($iTransactionRetryCount - $iTransactionRetry));
continue;
}
else
{
IssueLog::Error("Insert Deadlock TRANSACTION prevention failed.");
}
}
}
throw $e;
}
throw $e;
}
$this->m_bIsInDB = true;
@@ -3185,9 +3207,11 @@ abstract class DBObject implements iDisplay
$bIsTransactionEnabled = MetaModel::GetConfig()->Get('db_core_transactions_enabled');
if ($bIsTransactionEnabled)
{
$iIsTransactionRetryCount = MetaModel::GetConfig()->Get('db_core_transactions_retry_count');
// TODO Deep clone this object before the transaction (to use it in case of rollback)
// $iTransactionRetryCount = MetaModel::GetConfig()->Get('db_core_transactions_retry_count');
$iTransactionRetryCount = 1;
$iIsTransactionRetryDelay = MetaModel::GetConfig()->Get('db_core_transactions_retry_delay_ms');
$iTransactionRetry = $iIsTransactionRetryCount;
$iTransactionRetry = $iTransactionRetryCount;
}
while ($iTransactionRetry > 0)
{
@@ -3275,18 +3299,18 @@ abstract class DBObject implements iDisplay
}
catch (MySQLException $e)
{
IssueLog::Error($e->getMessage());
if ($bIsTransactionEnabled)
{
CMDBSource::Query('ROLLBACK');
if ($e->getCode() == 1213)
if (!CMDBSource::IsInsideTransaction() && CMDBSource::IsDeadlockException($e))
{
// Deadlock found when trying to get lock; try restarting transaction
IssueLog::Error($e->getMessage());
// Deadlock found when trying to get lock; try restarting transaction (only in main transaction)
if ($iTransactionRetry > 0)
{
// wait and retry
IssueLog::Error("Update TRANSACTION Retrying...");
usleep(random_int(1, 5) * 1000 * $iIsTransactionRetryDelay * ($iIsTransactionRetryCount - $iTransactionRetry));
usleep(random_int(1, 5) * 1000 * $iIsTransactionRetryDelay * ($iTransactionRetryCount - $iTransactionRetry));
continue;
}
else
@@ -3300,10 +3324,11 @@ abstract class DBObject implements iDisplay
'id' => $this->GetKey(),
'class' => get_class($this),
'issues' => $aErrors
));
), $e);
}
catch (CoreCannotSaveObjectException $e)
{
IssueLog::Error($e->getMessage());
if ($bIsTransactionEnabled)
{
CMDBSource::Query('ROLLBACK');
@@ -3312,6 +3337,7 @@ abstract class DBObject implements iDisplay
}
catch (Exception $e)
{
IssueLog::Error($e->getMessage());
if ($bIsTransactionEnabled)
{
CMDBSource::Query('ROLLBACK');
@@ -3514,9 +3540,11 @@ abstract class DBObject implements iDisplay
$bIsTransactionEnabled = MetaModel::GetConfig()->Get('db_core_transactions_enabled');
if ($bIsTransactionEnabled)
{
$iIsTransactionRetryCount = MetaModel::GetConfig()->Get('db_core_transactions_retry_count');
$iIsTransactionRetryDelay = MetaModel::GetConfig()->Get('db_core_transactions_retry_delay_ms');
$iTransactionRetry = $iIsTransactionRetryCount;
// TODO Deep clone this object before the transaction (to use it in case of rollback)
// $iTransactionRetryCount = MetaModel::GetConfig()->Get('db_core_transactions_retry_count');
$iTransactionRetryCount = 1;
$iTransactionRetryDelay = MetaModel::GetConfig()->Get('db_core_transactions_retry_delay_ms');
$iTransactionRetry = $iTransactionRetryCount;
}
while ($iTransactionRetry > 0)
{
@@ -3539,18 +3567,18 @@ abstract class DBObject implements iDisplay
}
catch (MySQLException $e)
{
IssueLog::Error($e->getMessage());
if ($bIsTransactionEnabled)
{
CMDBSource::Query('ROLLBACK');
if ($e->getCode() == 1213)
if (!CMDBSource::IsInsideTransaction() && CMDBSource::IsDeadlockException($e))
{
// Deadlock found when trying to get lock; try restarting transaction
IssueLog::Error($e->getMessage());
if ($iTransactionRetry > 0)
{
// wait and retry
IssueLog::Error("Delete TRANSACTION Retrying...");
usleep(random_int(1, 5) * 1000 * $iIsTransactionRetryDelay * ($iIsTransactionRetryCount - $iTransactionRetry));
usleep(random_int(1, 5) * 1000 * $iTransactionRetryDelay * ($iTransactionRetryCount - $iTransactionRetry));
continue;
}
else

View File

@@ -503,6 +503,7 @@ class FileLog
protected function Write($sText, $sLevel = '', $sChannel = '', $aContext = array())
{
$sTextPrefix = empty($sLevel) ? '' : (str_pad($sLevel, 7).' | ');
$sTextPrefix .= str_pad(UserRights::GetUserId(), 5)." | ";
$sTextSuffix = empty($sChannel) ? '' : " | $sChannel";
$sText = "{$sTextPrefix}{$sText}{$sTextSuffix}";
$sLogFilePath = $this->oFileNameBuilder->GetLogFilePath();

View File

@@ -242,6 +242,8 @@ class iTopMutex
*
* @throws \Exception
* @throws \MySQLException
*
* @since 2.7.5 3.0.0 N°3968 specify `wait_timeout` for the mutex dedicated connection
*/
public function InitMySQLSession()
{
@@ -254,10 +256,36 @@ class iTopMutex
$this->hDBLink = CMDBSource::GetMysqliInstance($sServer, $sUser, $sPwd, $sSource, $bTlsEnabled, $sTlsCA, false);
if (!$this->hDBLink)
{
if (!$this->hDBLink) {
throw new Exception("Could not connect to the DB server (host=$sServer, user=$sUser): ".mysqli_connect_error().' (mysql errno: '.mysqli_connect_errno().')');
}
// Make sure that the server variable `wait_timeout` is at least 86400 seconds for this connection,
// since the lock will be released if/when the connection times out.
// Source https://dev.mysql.com/doc/refman/5.7/en/locking-functions.html :
// > A lock obtained with GET_LOCK() is released explicitly by executing RELEASE_LOCK() or implicitly when your session terminates
//
// BEWARE: If you want to check the value of this variable, when run from an interactive console `SHOW VARIABLES LIKE 'wait_timeout'`
// will actually returns the value of the variable `interactive_timeout` which may be quite different.
$sSql = "SHOW VARIABLES LIKE 'wait_timeout'";
$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)) {
$iTimeout = (int)$aRow[1];
} else {
mysqli_free_result($result);
throw new Exception("No result for query '".$sSql."'");
}
mysqli_free_result($result);
if ($iTimeout < 86400) {
$result = mysqli_query($this->hDBLink, 'SET SESSION wait_timeout=86400');
if ($result === false) {
throw new Exception("Failed to issue MySQL query '".$sSql."': ".mysqli_error($this->hDBLink).' (mysql errno: '.mysqli_errno($this->hDBLink).')');
}
}
}

View File

@@ -68,6 +68,8 @@ Dict::Add('EN US', 'English', 'English', array(
Dict::Add('EN US', 'English', 'English', array(
'Portal:Form:Caselog:Entry:Close:Tooltip' => 'Close this entry',
'Portal:Form:Close:Warning' => 'Do you want to leave this form ? Data entered may be lost',
'Portal:Error:ObjectCannotBeCreated' => 'Error: object cannot be created. Check associated objects and attachments before submitting again this form.',
'Portal:Error:ObjectCannotBeUpdated' => 'Error: object cannot be updated. Check associated objects and attachments before submitting again this form.',
));
// UserProfile brick

View File

@@ -67,6 +67,8 @@ Dict::Add('FR FR', 'French', 'Français', array(
Dict::Add('FR FR', 'French', 'Français', array(
'Portal:Form:Caselog:Entry:Close:Tooltip' => 'Fermer cette entrée',
'Portal:Form:Close:Warning' => 'Voulez-vous quitter ce formulaire ? Les données saisies seront perdues',
'Portal:Error:ObjectCannotBeCreated' => 'Erreur: L\'objet n\'a pas été créé. Vérifiez les objets liés et les attachements avant de soumettre à nouveau le formulaire.',
'Portal:Error:ObjectCannotBeUpdated' => 'Erreur: L\'objet n\'a pas été modifié. Vérifiez les objets liés et les attachements avant de soumettre à nouveau le formulaire.',
));
// UserProfile brick

View File

@@ -30,6 +30,7 @@ use DBObjectSearch;
use DBObjectSet;
use DBSearch;
use FieldExpression;
use IssueLog;
use MetaModel;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
@@ -65,6 +66,8 @@ class BrowseBrickController extends BrickController
*/
public function DisplayAction(Request $oRequest, $sBrickId, $sBrowseMode = null, $sDataLoading = null)
{
$sPortalId = $this->getParameter('combodo.portal.instance.id');
/** @var \Combodo\iTop\Portal\Helper\BrowseBrickHelper $oBrowseBrickHelper */
$oBrowseBrickHelper = $this->get('browse_brick');
/** @var \Combodo\iTop\Portal\Helper\RequestManipulatorHelper $oRequestManipulator */
@@ -266,8 +269,7 @@ class BrowseBrickController extends BrickController
// Note : This could be way more simpler if we had a SetInternalParam($sParam, $value) verb
$aQueryParams = $aLevelsProperties[$aLevelsPropertiesKeys[$i]]['search']->GetInternalParams();
// Note : $iSearchloopMax was initialized on the previous loop
for ($j = 0; $j <= $iSearchLoopMax; $j++)
{
for ($j = 0; $j <= $iSearchLoopMax; $j++) {
$aQueryParams['search_value_'.$j] = '%'.$aSearchValues[$j].'%';
}
$aLevelsProperties[$aLevelsPropertiesKeys[$i]]['search']->SetInternalParams($aQueryParams);
@@ -277,12 +279,11 @@ class BrowseBrickController extends BrickController
$oQuery = $aLevelsProperties[$aLevelsPropertiesKeys[0]]['search'];
// Testing appropriate data loading mode if we are in auto
if ($sDataLoading === AbstractBrick::ENUM_DATA_LOADING_AUTO)
{
if ($sDataLoading === AbstractBrick::ENUM_DATA_LOADING_AUTO) {
// - Check how many records there is.
// - Update $sDataLoading with its new value regarding the number of record and the threshold
$oCountSet = new DBObjectSet($oQuery);
$fThreshold = (float)MetaModel::GetModuleSetting($this->getParameter('combodo.portal.instance.id'),
$fThreshold = (float)MetaModel::GetModuleSetting($sPortalId,
'lazy_loading_threshold');
$sDataLoading = ($oCountSet->Count() > $fThreshold) ? AbstractBrick::ENUM_DATA_LOADING_LAZY : AbstractBrick::ENUM_DATA_LOADING_FULL;
unset($oCountSet);
@@ -440,17 +441,21 @@ class BrowseBrickController extends BrickController
}
}
IssueLog::Debug('Portal BrowseBrick query', 'portal', array(
'portalId' => $sPortalId,
'brickId' => $sBrickId,
'oql' => $oSet->GetFilter()->ToOQL(),
));
// Preparing response
if ($oRequest->isXmlHttpRequest())
{
if ($oRequest->isXmlHttpRequest()) {
$aData = $aData + array(
'data' => $aItems,
'levelsProperties' => $aLevelsProperties,
);
$oResponse = new JsonResponse($aData);
}
else
{
} else {
$aData = $aData + array(
'oBrick' => $oBrick,
'sBrickId' => $sBrickId,

View File

@@ -42,6 +42,7 @@ use Dict;
use Exception;
use FieldExpression;
use iPopupMenuExtension;
use IssueLog;
use JSButtonItem;
use MetaModel;
use Symfony\Component\HttpFoundation\JsonResponse;
@@ -89,10 +90,10 @@ class ManageBrickController extends BrickController
/** @var \Combodo\iTop\Portal\Brick\ManageBrick $oBrick */
$oBrick = $oBrickCollection->GetBrickById($sBrickId);
if (is_null($sDisplayMode))
{
if (is_null($sDisplayMode)) {
$sDisplayMode = $oBrick->GetDefaultDisplayMode();
}
$aData = $this->GetData($oRequest, $sBrickId, $sGroupingTab, $oBrick::AreDetailsNeededForDisplayMode($sDisplayMode));
$aExportFields = $oBrick->GetExportFields();
@@ -102,8 +103,7 @@ class ManageBrickController extends BrickController
'iDefaultListLength' => $oBrick->GetDefaultListLength(),
);
// Preparing response
if ($oRequest->isXmlHttpRequest())
{
if ($oRequest->isXmlHttpRequest()) {
$oResponse = new JsonResponse($aData);
}
else
@@ -815,30 +815,31 @@ class ManageBrickController extends BrickController
'aColumnsDefinition' => $aColumnsDefinition,
);
}
}
else
{
IssueLog::Debug('Portal ManageBrick query', 'portal', array(
'portalId' => $sPortalId,
'brickId' => $sBrickId,
'groupingTab' => $sGroupingTab,
'oql' => $oSet->GetFilter()->ToOQL(),
'aGroupingTabs' => $aGroupingTabs,
));
} else {
$aGroupingAreasData = array();
$sGroupingArea = null;
}
// Preparing response
if ($oRequest->isXmlHttpRequest())
{
if ($oRequest->isXmlHttpRequest()) {
$aData = $aData + array(
'data' => $aGroupingAreasData[$sGroupingArea]['aItems'],
);
}
else
{
} else {
$aDisplayValues = array();
$aUrls = array();
$aColumns = array();
$aNames = array();
if ($bHasScope)
{
foreach ($aGroupingTabsValues as $aValues)
{
if ($bHasScope) {
foreach ($aGroupingTabsValues as $aValues) {
$aDisplayValues[] = array(
'value' => $aValues['count'],
'label' => $aValues['label'],

View File

@@ -25,14 +25,12 @@ use AttributeDateTime;
use AttributeTagSet;
use CMDBChangeOpAttachmentAdded;
use CMDBChangeOpAttachmentRemoved;
use CMDBSource;
use Combodo\iTop\Form\Field\Field;
use Combodo\iTop\Form\Field\FileUploadField;
use Combodo\iTop\Form\Field\LabelField;
use Combodo\iTop\Form\Form;
use Combodo\iTop\Form\FormManager;
use Combodo\iTop\Portal\Helper\ApplicationHelper;
use CoreCannotSaveObjectException;
use DBObject;
use DBObjectSearch;
use DBObjectSet;
@@ -1121,30 +1119,28 @@ class ObjectFormManager extends FormManager
return $aData;
}
// The try catch is essentially to start a MySQL transaction in order to ensure that all or none objects are persisted when creating an object with links
try
{
$sObjectClass = get_class($this->oObject);
$sObjectClass = get_class($this->oObject);
// Starting transaction
CMDBSource::Query('START TRANSACTION');
try {
// Forcing allowed writing on the object if necessary. This is used in some particular cases.
$bAllowWrite = ($sObjectClass === 'Person' && $this->oObject->GetKey() == UserRights::GetContactId());
if ($bAllowWrite)
{
if ($bAllowWrite) {
$this->oObject->AllowWrite(true);
}
// Writing object to DB
$bActivateTriggers = (!$this->oObject->IsNew() && $this->oObject->IsModified());
$bIsNew = $this->oObject->IsNew();
$bWasModified = $this->oObject->IsModified();
$bActivateTriggers = (!$bIsNew && $bWasModified);
try
{
$this->oObject->DBWrite();
}
catch (CoreCannotSaveObjectException $e)
{
throw new Exception($e->getHtmlMessage());
catch (Exception $e) {
if ($bIsNew) {
throw new Exception(Dict::S('Portal:Error:ObjectCannotBeCreated'));
}
throw new Exception(Dict::S('Portal:Error:ObjectCannotBeUpdated'));
}
// Finalizing images link to object, otherwise it will be cleaned by the GC
InlineImage::FinalizeInlineImages($this->oObject);
@@ -1155,9 +1151,6 @@ class ObjectFormManager extends FormManager
$this->FinalizeAttachments($aArgs['attachmentIds']);
}
// Ending transaction with a commit as everything was fine
CMDBSource::Query('COMMIT');
// Checking if we have to apply a stimulus
if (isset($aArgs['applyStimulus']))
{
@@ -1205,14 +1198,11 @@ class ObjectFormManager extends FormManager
}
catch (Exception $e)
{
// End transaction with a rollback as something failed
CMDBSource::Query('ROLLBACK');
$aData['valid'] = false;
$aData['messages']['error'] += array('_main' => array($e->getMessage()));
IssueLog::Error(__METHOD__.' at line '.__LINE__.' : Rollback during submit ('.$e->getMessage().')');
IssueLog::Error(__METHOD__.' at line '.__LINE__.' : '.$e->getMessage());
}
return $aData;
}