ITQuants blog

Sophis Risque: how to limit memory allocation on Citrix/Windows 7?

Jul 8

Written by:
7/8/2014 11:07 AM  RssIcon

In one of my previous posts that can be found there: http://www.it-quants.com/Blogs/tabid/83/EntryId/39/Sophis-Risque-how-to-manage-limit-memory-allocation.aspx, I've detailed a method that works fine on the following OS: Windows XP and Windows 8, with or without Citrix, using the MS CreateJobObject method.

Unfortunately, Microsoft has changed the behaviour of the jobs on Windows 7, and limit the creation to one job on this OS. Citrix is using already one job to control the limitations that can be driven through their configuration manager. For example, we will consider the case where the memory consumption is configured per Sophis user. The goal of this post is to explain the solution that could be retained, without exporting the rights to the Citrix configuration and let keeping Risque/Value manage the memory consumption as before.

In a nutshell, the only solution is to create a thread that will check the memory by polling it every x seconds. The memory configuration for each user is stored in database. The implementation of the memory check will:

- load the configuration for the user

- start the thread and giving the limit to check at the same time

- use the psapi callbacks in order to detect f the Peak Working Set Memory (pwss) is exceeded or not

- when the pwss limit is reached, give the possiblity (timeout) to quit properly the application,

- after xx seconds of timeout, kill the process by calling MS SDK method TerminateProcess

- otherwise, when Sophis quits, stop the memory check thread.

Finally, the code for the thread should look like this one:

Header:

 

#pragma once
  
class CMemoryCheckThread
{
public:
    static CMemoryCheckThread& getInstance();
  
    bool Start(double memoryLimit);
    void Stop();
protected:
    HANDLE m_hEvent;
    HANDLE m_hThread;
    long m_timeout;
    long m_timeoutOnExit;
    double m_memoryLimit;
    static DWORD CheckMemory(void *param);
    static DWORD KillProcess(void *param);
private:
    CMemoryCheckThread();
    static CMemoryCheckThread g_instance;
};

 

.cpp:

#include "stdafx.h"
#include "MemoryCheckThread.h"
#include
  
CMemoryCheckThread CMemoryCheckThread::g_instance;
  
CMemoryCheckThread& CMemoryCheckThread::getInstance()
{
    return g_instance;
}
  
CMemoryCheckThread::CMemoryCheckThread()
: m_hThread(NULL), 
 m_hEvent(NULL), 
 m_timeout(30), 
 m_timeoutOnExit(300),
 m_memoryLimit(0.)
{
}
  
bool CMemoryCheckThread::Start(double memoryLimit)
{
    bool _result = false;
    if(m_hThread==NULL)
    {
        m_timeout  = 30; //30s
        m_timeoutOnExit = 300; //5'
        try
        {
            sophis::misc::ConfigurationFileWrapper::getEntryValue("PERF","timeout",m_timeout);
        }
        catch(...)
        {
        }
        try
        {
            sophis::misc::ConfigurationFileWrapper::getEntryValue("PERF","timeoutOnExit",m_timeoutOnExit);
        }
        catch(...)
        {
        }
        m_hEvent = ::CreateEvent(NULL,FALSE,FALSE,NULL);
        DWORD dwThreadID = 0;
        if(m_hEvent!=NULL)
        {
            m_memoryLimit = memoryLimit;
            m_hThread = CreateThread(NULL,             // default security
                                    0,                 // default stack size
                                    CheckMemory,        // name of the thread function
                                    this,              // the object in parameter
                                    0,                 // default startup flags
                                    &dwThreadID); 
            _result = m_hThread!=NULL;
        }
    }
    return _result;
}
  
void CMemoryCheckThread::Stop()
{
    if(m_hEvent!=NULL)
        ::SetEvent(m_hEvent);
    if(m_hThread!=NULL)
    {
        WaitForSingleObject(m_hThread,30000);
        ::CloseHandle(m_hThread);
    }
    if(m_hEvent!=NULL)
        CloseHandle(m_hEvent);
}
  
DWORD CMemoryCheckThread::KillProcess(void* param)
{
    CMemoryCheckThread* _instance = (CMemoryCheckThread*)param;
    if(_instance!=NULL)
    {
        DWORD dwRet = ::WaitForSingleObject(_instance->m_hEvent, _instance->m_timeoutOnExit * 1000);
        if(dwRet!=WAIT_OBJECT_0)
        {
            TerminateProcess(GetCurrentProcess(),-1);
        }
    }
    return 0;
}
  
DWORD CMemoryCheckThread::CheckMemory(void *param)
{
    CMemoryCheckThread* _instance = (CMemoryCheckThread*)param;
    while(true && _instance)
    {
        DWORD dwRet = ::WaitForSingleObject(_instance->m_hEvent, _instance->m_timeout * 1000);
        if(dwRet==WAIT_TIMEOUT)
        {
            HANDLE hProcess = GetCurrentProcess();                  
            PROCESS_MEMORY_COUNTERS pmc;
                                              
            if(hProcess)                    
            {   
                if(GetProcessMemoryInfo( hProcess, &pmc, sizeof( pmc ) ) ) 
                {           
                    double pwss = ((float)pmc.PeakWorkingSetSize) / (double)(1024.0 * 1024.0 * 1024) ;
                    if(pwss>_instance->m_memoryLimit)
                    {
                        DWORD dwThreadID;
                        // need to create a thread since MessageBox will keep the hand after until the user release it
                        HANDLE hThread = CreateThread(NULL,         // default security
                                                0,                  // default stack size
                                                KillProcess,        // name of the thread function
                                                _instance,          // the object in parameter
                                                0,                 // default startup flags
                                                &dwThreadID); 
                        if(hThread!=NULL)
                        {
                            char _buffer[256];
                            _snprintf(_buffer,sizeof(_buffer),"Memory limit (%.2f GB>%.2f GB) reached, please kill your Sophis session!\nIf nothing is done, it will be killed in %i mn.\nContact the support if you need more memory.",pwss,_instance->m_memoryLimit,_instance->m_timeoutOnExit/60);                                   MessageBox(NULL,_buffer,"Sophis Risque",MB_SERVICE_NOTIFICATION|MB_ICONERROR);
                            WaitForSingleObject(hThread,INFINITE);
                            CloseHandle(hThread);
                        }
                        else
                            TerminateProcess(hProcess,-1);
                    }
                }
            }
        }
        else // exit the thread when no timeout
            break;
    }
    return 0;
}

 

In the Sophis UNIVERSAL_MAIN, just a call to

CMemoryCheckThread::getInstance().Start(4.0); // to limit to 4.0 Gb 

 

And in the DllMain entry point, on the DLL_PROCESS_DETACH, the thread has to be stopped by calling:

CMemoryCheckThread::getInstance().Stop();

 

On Windows 8, CreateJobObject can create several jobs per process as on XP, the limitation encountered on Windows 7 was removed by Microsoft.

Tags:
Categories: Sophis, C++

Search blog