[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