[mapguide-commits] r10255 - in branches/4.0/MgDev/Server/src: Common/Base Common/Manager Services/Resource UnitTesting
svn_mapguide at osgeo.org
svn_mapguide at osgeo.org
Mon Nov 2 21:06:46 PST 2026
Author: jng
Date: 2026-11-02 21:06:46 -0800 (Mon, 02 Nov 2026)
New Revision: 10255
Modified:
branches/4.0/MgDev/Server/src/Common/Base/ServiceOperation.cpp
branches/4.0/MgDev/Server/src/Common/Manager/LogManager.cpp
branches/4.0/MgDev/Server/src/Common/Manager/LogManager.h
branches/4.0/MgDev/Server/src/Services/Resource/DbEnvironment.cpp
branches/4.0/MgDev/Server/src/UnitTesting/TestLogManager.cpp
Log:
Improve quality of some BDB errors masked as out of memory errors. Fixes #2292
Modified: branches/4.0/MgDev/Server/src/Common/Base/ServiceOperation.cpp
===================================================================
--- branches/4.0/MgDev/Server/src/Common/Base/ServiceOperation.cpp 2026-11-03 04:56:16 UTC (rev 10254)
+++ branches/4.0/MgDev/Server/src/Common/Base/ServiceOperation.cpp 2026-11-03 05:06:46 UTC (rev 10255)
@@ -229,6 +229,7 @@
}
STRING message = except->GetExceptionMessage(locale);
+ message += MgLogManager::FormatExceptionInfo(except);
STRING details = except->GetDetails(locale);
STRING stackTrace = except->GetStackTrace(locale);
Modified: branches/4.0/MgDev/Server/src/Common/Manager/LogManager.cpp
===================================================================
--- branches/4.0/MgDev/Server/src/Common/Manager/LogManager.cpp 2026-11-03 04:56:16 UTC (rev 10254)
+++ branches/4.0/MgDev/Server/src/Common/Manager/LogManager.cpp 2026-11-03 05:06:46 UTC (rev 10255)
@@ -20,6 +20,16 @@
#include "LogManager.h"
#include "LogThread.h"
+#include <errno.h>
+
+#ifdef _WIN32
+#include <Psapi.h>
+#else
+#include <cstdio>
+#include <cstdlib>
+#include <cstring>
+#endif
+
const int MAX_BUF = 16384;
const int MAX_LINES = 512;
const int SECONDSINDAY = 86400;
@@ -2188,6 +2198,115 @@
QueueLogEntry(mltTrace, logEntry, LM_INFO);
}
+///////////////////////////////////////////////////////////////////////////////
+/// Reads the process's virtual size and resident set size, in bytes.
+/// A negative value means "not available".
+///
+static void GetProcessMemoryUsage(INT64& virtualMemory, INT64& workingSet)
+{
+ virtualMemory = -1;
+ workingSet = -1;
+
+#ifdef _WIN32
+ PROCESS_MEMORY_COUNTERS counters;
+ counters.cb = sizeof(counters);
+ if (::GetProcessMemoryInfo(::GetCurrentProcess(), &counters, sizeof(counters)))
+ {
+ workingSet = (INT64)counters.WorkingSetSize;
+ virtualMemory = (INT64)counters.PagefileUsage;
+ }
+#else
+ FILE* fp = ::fopen("/proc/self/status", "rb");
+ if (NULL != fp)
+ {
+ char buffer[4096];
+ size_t count = ::fread(buffer, 1, sizeof(buffer) - 1, fp);
+ ::fclose(fp);
+ buffer[count] = '\0';
+
+ const char* value = ::strstr(buffer, "VmSize:");
+ if (NULL != value)
+ {
+ virtualMemory = strtoll(value + 7, NULL, 10) * 1024;
+ }
+
+ value = ::strstr(buffer, "VmRSS:");
+ if (NULL != value)
+ {
+ workingSet = strtoll(value + 6, NULL, 10) * 1024;
+ }
+ }
+#endif
+}
+
+///////////////////////////////////////////////////////////////////////////////
+/// \brief
+/// Returns the facts about an exception that the log entry would otherwise
+/// lose: its code and, when set, its native error code.
+///
+/// For allocation failures the process's memory state is added as well, since
+/// Berkeley DB reports a full lock table as ENOMEM too - the values show
+/// whether the process was anywhere near a memory limit when it happened.
+///
+STRING MgLogManager::FormatExceptionInfo(MgException* except)
+{
+ STRING info;
+
+ try
+ {
+ if (NULL == except)
+ {
+ return info;
+ }
+
+ STRING code = except->GetExceptionCode();
+ if (!code.empty())
+ {
+ info += L" [code=";
+ info += code;
+ info += L"]";
+ }
+
+ INT64 nativeErrorCode = except->GetNativeErrorCode();
+ if (0 != nativeErrorCode)
+ {
+ STRING value;
+ MgUtil::Int64ToString(nativeErrorCode, value);
+
+ info += L" [nativeError=";
+ info += value;
+ info += L"]";
+ }
+
+ bool allocationFailure = (MgExceptionCodes::MgOutOfMemoryException == code)
+ || (0 != nativeErrorCode && ENOMEM == nativeErrorCode);
+
+ if (allocationFailure)
+ {
+ INT64 virtualMemory = -1;
+ INT64 workingSet = -1;
+ GetProcessMemoryUsage(virtualMemory, workingSet);
+
+ STRING vmValue;
+ STRING rssValue;
+ MgUtil::Int64ToString(virtualMemory, vmValue);
+ MgUtil::Int64ToString(workingSet, rssValue);
+
+ info += L" [vm=";
+ info += vmValue;
+ info += L" rss=";
+ info += rssValue;
+ info += L"]";
+ }
+ }
+ catch (...)
+ {
+ // Formatting diagnostics must never lose the entry being logged.
+ }
+
+ return info;
+}
+
void MgLogManager::LogSystemErrorEntry(MgException* except)
{
if (NULL != except)
@@ -2197,6 +2316,7 @@
STRING locale = serverManager->GetDefaultMessageLocale();
STRING message = except->GetExceptionMessage(locale);
+ message += MgLogManager::FormatExceptionInfo(except);
STRING details = except->GetDetails(locale);
STRING stackTrace = except->GetStackTrace(locale);
Modified: branches/4.0/MgDev/Server/src/Common/Manager/LogManager.h
===================================================================
--- branches/4.0/MgDev/Server/src/Common/Manager/LogManager.h 2026-11-03 04:56:16 UTC (rev 10254)
+++ branches/4.0/MgDev/Server/src/Common/Manager/LogManager.h 2026-11-03 05:06:46 UTC (rev 10255)
@@ -309,6 +309,7 @@
void Initialize();
void LoadConfigurationProperties();
STRING GetLogsPath();
+ static STRING FormatExceptionInfo(MgException* except);
void SetMaximumLogSize(INT32 size);
void SetLogDelimiter(CREFSTRING delimiter);
STRING GetLogDelimiter();
Modified: branches/4.0/MgDev/Server/src/Services/Resource/DbEnvironment.cpp
===================================================================
--- branches/4.0/MgDev/Server/src/Services/Resource/DbEnvironment.cpp 2026-11-03 04:56:16 UTC (rev 10254)
+++ branches/4.0/MgDev/Server/src/Services/Resource/DbEnvironment.cpp 2026-11-03 05:06:46 UTC (rev 10255)
@@ -18,6 +18,125 @@
#include "ResourceServiceDefs.h"
#include "DbEnvironment.h"
+#include <cstdio>
+#include <cstring>
+#ifdef _WIN32
+#include <fcntl.h>
+#include <io.h>
+#else
+#include <fcntl.h>
+#include <unistd.h>
+#endif
+
+///////////////////////////////////////////////////////////////////////////////
+/// Berkeley DB reports its own diagnostics through an error callback rather
+/// than through the exception it returns: "Lock table is out of available
+/// locker entries", "unable to allocate memory for the lock table", "PANIC:
+/// fatal region error detected" and so on. Without a callback those messages
+/// go to stderr and are lost, and the exception itself only carries
+/// strerror() of the error number - which is how an exhausted lock table ends
+/// up logged as "Cannot allocate memory".
+///
+/// The callback can run while Berkeley DB holds region mutexes and while the
+/// process cannot allocate memory, so it writes to a pre-opened file
+/// descriptor: no allocation, no logging framework, nothing that can throw.
+/// The file is truncated once per process, so it describes the current run.
+///
+static const wchar_t* MG_DB_ERROR_LOG_FILE = L"DbError.log";
+
+#ifdef _WIN32
+#define MG_DB_ERROR_OPEN _open
+#define MG_DB_ERROR_WRITE _write
+#define MG_DB_ERROR_OPEN_FLAGS (_O_CREAT|_O_WRONLY|_O_TRUNC|_O_APPEND|_O_BINARY)
+#else
+#define MG_DB_ERROR_OPEN ::open
+#define MG_DB_ERROR_WRITE ::write
+#define MG_DB_ERROR_OPEN_FLAGS (O_CREAT|O_WRONLY|O_TRUNC|O_APPEND)
+#endif
+
+static int OpenDbErrorLog()
+{
+ try
+ {
+ STRING logsPath;
+ MgConfiguration* configuration = MgConfiguration::GetInstance();
+ if (NULL != configuration)
+ {
+ configuration->GetStringValue(
+ MgConfigProperties::GeneralPropertiesSection,
+ MgConfigProperties::GeneralPropertyLogsPath,
+ logsPath,
+ MgConfigProperties::DefaultGeneralPropertyLogsPath);
+ }
+
+ if (logsPath.empty())
+ {
+ return -1;
+ }
+
+ // LogsPath is a directory in the configuration; it need not end with a
+ // separator (LogManager normalises it the same way).
+ MgFileUtil::AppendSlashToEndOfPath(logsPath);
+ MgFileUtil::CreateDirectory(logsPath, false, true);
+ logsPath += MG_DB_ERROR_LOG_FILE;
+
+ string path = MgUtil::WideCharToMultiByte(logsPath);
+ return MG_DB_ERROR_OPEN(path.c_str(), MG_DB_ERROR_OPEN_FLAGS, 0644);
+ }
+ catch (...)
+ {
+ // Diagnostics must never prevent a repository from being opened.
+ return -1;
+ }
+}
+
+static int GetDbErrorLog()
+{
+ // C++11 makes the initialization of a function-local static thread-safe, so
+ // the file is opened exactly once per process regardless of how many
+ // environments (one per session with FilePerSession) are created.
+ static const int fd = OpenDbErrorLog();
+ return fd;
+}
+
+static const char* GetDbErrorPrefix(CREFSTRING repositoryType)
+{
+ if (MgRepositoryType::Session == repositoryType)
+ {
+ return "Session";
+ }
+ if (MgRepositoryType::Site == repositoryType)
+ {
+ return "Site";
+ }
+ return "Library";
+}
+
+static void WriteDbErrorMessage(const DbEnv* dbEnv, const char* errPrefix, const char* message)
+{
+ int fd = GetDbErrorLog();
+ if (fd < 0 || NULL == message)
+ {
+ return;
+ }
+
+ char line[1024];
+ int len = (NULL != errPrefix)
+ ? snprintf(line, sizeof(line), "[DB] %s: %s\n", errPrefix, message)
+ : snprintf(line, sizeof(line), "[DB] %s\n", message);
+
+ if (len <= 0)
+ {
+ return;
+ }
+
+ size_t count = ((size_t)len < sizeof(line)) ? (size_t)len : sizeof(line) - 1;
+
+ // There is nowhere left to report a failed write to.
+ int written = (int)MG_DB_ERROR_WRITE(fd, line, (unsigned int)count);
+ ACE_UNUSED_ARG(written);
+}
+
///----------------------------------------------------------------------------
/// <summary>
/// Constructs the object.
@@ -194,6 +313,13 @@
environmentFlags |= DB_INIT_CDB;
}
+ // Route Berkeley DB's own diagnostics to the logs directory. Set before the
+ // environment is opened so that failures during open (for example "unable
+ // to allocate memory for the lock table") are captured too.
+ GetDbErrorLog();
+ m_dbEnv.set_errpfx(GetDbErrorPrefix(repositoryType));
+ m_dbEnv.set_errcall(&WriteDbErrorMessage);
+
m_dbEnv.open(home.c_str(), environmentFlags, 0);
m_opened = true;
@@ -217,6 +343,11 @@
m_xmlMan.setDefaultContainerFlags(containerFlags);
+ // Re-assert the error callback: DbXml installs its own (std::cout based)
+ // handler on an adopted environment when it is built with DB_SET_ERRCALL.
+ m_dbEnv.set_errpfx(GetDbErrorPrefix(repositoryType));
+ m_dbEnv.set_errcall(&WriteDbErrorMessage);
+
MG_RESOURCE_SERVICE_CATCH_AND_THROW(L"MgDbEnvironment.MgDbEnvironment")
}
Modified: branches/4.0/MgDev/Server/src/UnitTesting/TestLogManager.cpp
===================================================================
--- branches/4.0/MgDev/Server/src/UnitTesting/TestLogManager.cpp 2026-11-03 04:56:16 UTC (rev 10254)
+++ branches/4.0/MgDev/Server/src/UnitTesting/TestLogManager.cpp 2026-11-03 05:06:46 UTC (rev 10255)
@@ -23,7 +23,9 @@
#include "TestServiceFactory.h"
#include "catch.hpp"
+#include <errno.h>
+
const wchar_t TestName[] = L"TestName.log";
const wchar_t TestName2[] = L"TestName2.log";
const wchar_t NewTestName[] = L"SuperCoolNewTestName.log";
@@ -1860,3 +1862,40 @@
REQUIRE(logContents == contents);
}
+
+///----------------------------------------------------------------------------
+/// Test Case Description:
+///
+/// The error log records the exception code and native error code alongside
+/// the message, plus the process memory state for allocation failures. Without
+/// it a DB error reported as ENOMEM ("Cannot allocate memory") cannot be told
+/// apart from the process actually running out of memory.
+///----------------------------------------------------------------------------
+TEST_CASE("FormatExceptionInfo", "[LogManager]")
+{
+ // A database error reported as ENOMEM carries the code, the native error
+ // and the process memory state.
+ Ptr<MgException> dbException = new MgException(
+ MgExceptionCodes::MgDbXmlException, L"TestLogManager.FormatExceptionInfo",
+ __LINE__, __WFILE__, NULL, L"", NULL);
+ dbException->SetNativeErrorCode(ENOMEM);
+
+ STRING info = MgLogManager::FormatExceptionInfo(dbException);
+ REQUIRE(info.find(L"[code=MgDbXmlException]") != STRING::npos);
+ REQUIRE(info.find(L"[nativeError=") != STRING::npos);
+ REQUIRE(info.find(L"[vm=") != STRING::npos);
+
+ // An exception with no native error code reports neither the native error
+ // nor the memory state.
+ Ptr<MgException> plainException = new MgException(
+ MgExceptionCodes::MgNullReferenceException, L"TestLogManager.FormatExceptionInfo",
+ __LINE__, __WFILE__, NULL, L"", NULL);
+
+ STRING plainInfo = MgLogManager::FormatExceptionInfo(plainException);
+ REQUIRE(plainInfo.find(L"[code=MgNullReferenceException]") != STRING::npos);
+ REQUIRE(plainInfo.find(L"[nativeError=") == STRING::npos);
+ REQUIRE(plainInfo.find(L"[vm=") == STRING::npos);
+
+ // A null exception yields nothing rather than throwing.
+ REQUIRE(MgLogManager::FormatExceptionInfo(NULL).empty());
+}
More information about the mapguide-commits
mailing list