diff --git a/src/config/iisnode_schema.xml b/src/config/iisnode_schema.xml index ecf2313..e647bf7 100644 --- a/src/config/iisnode_schema.xml +++ b/src/config/iisnode_schema.xml @@ -29,7 +29,7 @@ Details at http://learn.iis.net/page.aspx/241/configuration-extensibility/ - + diff --git a/src/config/iisnode_schema_x64.xml b/src/config/iisnode_schema_x64.xml index 3ea80f2..d137bdc 100644 --- a/src/config/iisnode_schema_x64.xml +++ b/src/config/iisnode_schema_x64.xml @@ -29,7 +29,7 @@ Details at http://learn.iis.net/page.aspx/241/configuration-extensibility/ - + diff --git a/src/iisnode/cmoduleconfiguration.cpp b/src/iisnode/cmoduleconfiguration.cpp index cb9e72a..cf07a9d 100644 --- a/src/iisnode/cmoduleconfiguration.cpp +++ b/src/iisnode/cmoduleconfiguration.cpp @@ -434,7 +434,7 @@ HRESULT CModuleConfiguration::GetConfig(IHttpContext* context, CModuleConfigurat CheckError(GetConfigSection(context, §ion)); CheckError(GetDWORD(section, L"asyncCompletionThreadCount", &c->asyncCompletionThreadCount)); - CheckError(GetDWORD(section, L"maxProcessCountPerApplication", &c->maxProcessCountPerApplication)); + CheckError(GetDWORD(section, L"nodeProcessCountPerApplication", &c->nodeProcessCountPerApplication)); CheckError(GetDWORD(section, L"maxConcurrentRequestsPerProcess", &c->maxConcurrentRequestsPerProcess)); CheckError(GetDWORD(section, L"maxNamedPipeConnectionRetry", &c->maxNamedPipeConnectionRetry)); CheckError(GetDWORD(section, L"namedPipeConnectionRetryDelay", &c->namedPipeConnectionRetryDelay)); @@ -474,6 +474,16 @@ HRESULT CModuleConfiguration::GetConfig(IHttpContext* context, CModuleConfigurat GetSystemInfo(&info); c->asyncCompletionThreadCount = 0 == info.dwNumberOfProcessors ? 4 : info.dwNumberOfProcessors; } + + if (0 == c->nodeProcessCountPerApplication) + { + // default number of node.exe processes to create per node.js application + + SYSTEM_INFO info; + + GetSystemInfo(&info); + c->nodeProcessCountPerApplication = 0 == info.dwNumberOfProcessors ? 1 : info.dwNumberOfProcessors; + } // CR: check for ERROR_ALREADY_ASSIGNED to detect a race in creation of this section // CR: refcounting may be needed if synchronous code paths proove too long (race with config changes) @@ -521,9 +531,9 @@ DWORD CModuleConfiguration::GetAsyncCompletionThreadCount(IHttpContext* ctx) GETCONFIG(asyncCompletionThreadCount) } -DWORD CModuleConfiguration::GetMaxProcessCountPerApplication(IHttpContext* ctx) +DWORD CModuleConfiguration::GetNodeProcessCountPerApplication(IHttpContext* ctx) { - GETCONFIG(maxProcessCountPerApplication) + GETCONFIG(nodeProcessCountPerApplication) } LPCTSTR CModuleConfiguration::GetNodeProcessCommandLine(IHttpContext* ctx) diff --git a/src/iisnode/cmoduleconfiguration.h b/src/iisnode/cmoduleconfiguration.h index 8b8b2b0..b71c735 100644 --- a/src/iisnode/cmoduleconfiguration.h +++ b/src/iisnode/cmoduleconfiguration.h @@ -8,7 +8,7 @@ class CModuleConfiguration : public IHttpStoredContext private: DWORD asyncCompletionThreadCount; - DWORD maxProcessCountPerApplication; + DWORD nodeProcessCountPerApplication; LPTSTR nodeProcessCommandLine; DWORD maxConcurrentRequestsPerProcess; DWORD maxNamedPipeConnectionRetry; @@ -48,7 +48,7 @@ public: static HRESULT Initialize(IHttpServer* server, HTTP_MODULE_ID moduleId); static DWORD GetAsyncCompletionThreadCount(IHttpContext* ctx); - static DWORD GetMaxProcessCountPerApplication(IHttpContext* ctx); + static DWORD GetNodeProcessCountPerApplication(IHttpContext* ctx); static LPCTSTR GetNodeProcessCommandLine(IHttpContext* ctx); static DWORD GetMaxConcurrentRequestsPerProcess(IHttpContext* ctx); static DWORD GetMaxNamedPipeConnectionRetry(IHttpContext* ctx); diff --git a/src/iisnode/cnodeprocessmanager.cpp b/src/iisnode/cnodeprocessmanager.cpp index cf6f3fa..b61a9cd 100644 --- a/src/iisnode/cnodeprocessmanager.cpp +++ b/src/iisnode/cnodeprocessmanager.cpp @@ -1,16 +1,16 @@ #include "precomp.h" CNodeProcessManager::CNodeProcessManager(CNodeApplication* application, IHttpContext* context) - : application(application), processes(NULL), processCount(0), currentProcess(0), isClosing(FALSE), + : application(application), processes(NULL), currentProcess(0), isClosing(FALSE), refCount(1) { if (this->GetApplication()->IsDebugMode()) { - this->maxProcessCount = 1; + this->processCount = 1; } else { - this->maxProcessCount = CModuleConfiguration::GetMaxProcessCountPerApplication(context); + this->processCount = CModuleConfiguration::GetNodeProcessCountPerApplication(context); } // cache event provider since the application can be disposed prior to CNodeProcessManager @@ -22,11 +22,19 @@ CNodeProcessManager::CNodeProcessManager(CNodeApplication* application, IHttpCon CNodeProcessManager::~CNodeProcessManager() { - if (NULL != processes) + this->Cleanup(); +} + +void CNodeProcessManager::Cleanup() +{ + if (NULL != this->processes) { for (int i = 0; i < this->processCount; i++) { - delete this->processes[i]; + if (this->processes[i]) + { + delete this->processes[i]; + } } delete[] this->processes; @@ -43,74 +51,45 @@ HRESULT CNodeProcessManager::Initialize(IHttpContext* context) { HRESULT hr; - ErrorIf(NULL == (this->processes = new CNodeProcess* [this->maxProcessCount]), ERROR_NOT_ENOUGH_MEMORY); - RtlZeroMemory(this->processes, this->maxProcessCount * sizeof(CNodeProcess*)); - if (this->GetApplication()->IsDebuggee()) + ErrorIf(NULL == (this->processes = new CNodeProcess* [this->processCount]), ERROR_NOT_ENOUGH_MEMORY); + RtlZeroMemory(this->processes, this->processCount * sizeof(CNodeProcess*)); + for (int i = 0; i < this->processCount; i++) { - // ensure the debugee process is started without activating message - // this is to make sure it is available for the debugger to connect to - - CheckError(this->AddOneProcess(NULL, context)); + CheckError(this->AddProcess(i, context)); } return S_OK; Error: - if (NULL != this->processes) - { - delete [] this->processes; - this->processes = NULL; - } + this->Cleanup(); return hr; } -HRESULT CNodeProcessManager::AddOneProcessCore(CNodeProcess** process, IHttpContext* context) +HRESULT CNodeProcessManager::AddProcess(int ordinal, IHttpContext* context) { HRESULT hr; - ErrorIf(this->processCount == this->maxProcessCount, ERROR_NOT_ENOUGH_QUOTA); - ErrorIf(NULL == (this->processes[this->processCount] = new CNodeProcess(this, context, this->processCount)), ERROR_NOT_ENOUGH_MEMORY); - CheckError(this->processes[this->processCount]->Initialize(context)); - if (NULL != process) - { - *process = this->processes[this->processCount]; - } - this->processCount++; + ErrorIf(NULL != this->processes[ordinal], ERROR_INVALID_PARAMETER); + ErrorIf(NULL == (this->processes[ordinal] = new CNodeProcess(this, context, ordinal)), ERROR_NOT_ENOUGH_MEMORY); + CheckError(this->processes[ordinal]->Initialize(context)); return S_OK; Error: - if (NULL != this->processes[this->processCount]) + if (NULL != this->processes[ordinal]) { - delete this->processes[this->processCount]; - this->processes[this->processCount] = NULL; + delete this->processes[ordinal]; + this->processes[ordinal] = NULL; } return hr; } -HRESULT CNodeProcessManager::AddOneProcess(CNodeProcess** process, IHttpContext* context) -{ - HRESULT hr; - - if (NULL != process) - { - *process = NULL; - } - - ErrorIf(this->processCount == this->maxProcessCount, ERROR_NOT_ENOUGH_QUOTA); - CheckError(this->AddOneProcessCore(process, context)); - - return S_OK; -Error: - - return hr; -} - HRESULT CNodeProcessManager::Dispatch(CNodeHttpStoredContext* request) { HRESULT hr; + unsigned int tmpProcess, processToUse; CheckNull(request); @@ -120,24 +99,48 @@ HRESULT CNodeProcessManager::Dispatch(CNodeHttpStoredContext* request) { ENTER_SRW_SHARED(this->srwlock) - if (!this->isClosing && this->TryRouteRequestToExistingProcess(request)) + if (!this->isClosing) { - request = NULL; + // employ a round robin routing logic to get a "ticket" to use a process with a specific ordinal number + + if (1 == this->processCount) + { + processToUse = 0; + } + else + { + do + { + tmpProcess = this->currentProcess; + processToUse = (tmpProcess + 1) % this->processCount; + } while (tmpProcess != InterlockedCompareExchange(&this->currentProcess, processToUse, tmpProcess)); + } + + // try dispatch to that process + + if (NULL != this->processes[processToUse]) + { + CheckError(this->processes[processToUse]->AcceptRequest(request)); + request = NULL; + } } LEAVE_SRW_SHARED(this->srwlock) - if (request && !this->isClosing) + if (NULL != request) { - // existing processes were unable to accept this request; create a new process to handle it + // the process to dispatch to does not exist and must be recreated ENTER_SRW_EXCLUSIVE(this->srwlock) - if (!this->isClosing && !this->TryRouteRequestToExistingProcess(request)) + if (!this->isClosing) { - CNodeProcess* newProcess = NULL; - CheckError(this->AddOneProcess(&newProcess, request->GetHttpContext())); - CheckError(newProcess->AcceptRequest(request)); + if (NULL == this->processes[processToUse]) + { + CheckError(this->AddProcess(processToUse, request->GetHttpContext())); + } + + CheckError(this->processes[processToUse]->AcceptRequest(request)); } LEAVE_SRW_EXCLUSIVE(this->srwlock) @@ -162,29 +165,6 @@ Error: return hr; } -BOOL CNodeProcessManager::TryRouteRequestToExistingProcess(CNodeHttpStoredContext* context) -{ - if (this->processCount == 0) - { - return false; - } - - DWORD i = this->currentProcess; - - do { - - if (S_OK == this->processes[i]->AcceptRequest(context)) - { - return true; - } - - i = (i + 1) % this->processCount; - - } while (i != this->currentProcess); - - return false; -} - HRESULT CNodeProcessManager::RecycleProcess(CNodeProcess* process) { HRESULT hr; @@ -221,13 +201,7 @@ HRESULT CNodeProcessManager::RecycleProcess(CNodeProcess* process) return S_OK; } - if (i < (this->processCount - 1)) - { - memcpy(this->processes + i, this->processes + i + 1, sizeof (CNodeProcess*) * (this->processCount - i - 1)); - } - - this->processCount--; - this->currentProcess = 0; + this->processes[i] = NULL; gracefulRecycle = TRUE; } @@ -277,7 +251,17 @@ HRESULT CNodeProcessManager::Recycle() this->isClosing = TRUE; - if (0 < this->processCount) + BOOL hasActiveProcess = FALSE; + for (int i = 0; i < this->processCount; i++) + { + if (this->processes[i]) + { + hasActiveProcess = TRUE; + break; + } + } + + if (hasActiveProcess) { // perform actual recycling on a diffrent thread to free up the file watcher thread @@ -321,6 +305,7 @@ unsigned int CNodeProcessManager::GracefulShutdown(void* arg) ProcessRecycleArgs* args = (ProcessRecycleArgs*)arg; HRESULT hr; HANDLE* drainHandles = NULL; + DWORD drainHandleCount = 0; // drain active requests @@ -328,16 +313,20 @@ unsigned int CNodeProcessManager::GracefulShutdown(void* arg) RtlZeroMemory(drainHandles, args->count * sizeof HANDLE); for (int i = 0; i < args->count; i++) { - drainHandles[i] = CreateEvent(NULL, TRUE, FALSE, NULL); - args->processes[i]->SignalWhenDrained(drainHandles[i]); + if (args->processes[i]) + { + drainHandles[drainHandleCount] = CreateEvent(NULL, TRUE, FALSE, NULL); + args->processes[i]->SignalWhenDrained(drainHandles[drainHandleCount]); + drainHandleCount++; + } } if (args->processManager->gracefulShutdownTimeout > 0) { - WaitForMultipleObjects(args->count, drainHandles, TRUE, args->processManager->gracefulShutdownTimeout); + WaitForMultipleObjects(drainHandleCount, drainHandles, TRUE, args->processManager->gracefulShutdownTimeout); } - for (int i = 0; i < args->count; i++) + for (int i = 0; i < drainHandleCount; i++) { CloseHandle(drainHandles[i]); } diff --git a/src/iisnode/cnodeprocessmanager.h b/src/iisnode/cnodeprocessmanager.h index 5afa6d9..23f5702 100644 --- a/src/iisnode/cnodeprocessmanager.h +++ b/src/iisnode/cnodeprocessmanager.h @@ -22,18 +22,16 @@ private: CNodeApplication* application; CNodeProcess** processes; DWORD processCount; - DWORD maxProcessCount; - DWORD currentProcess; + unsigned int currentProcess; SRWLOCK srwlock; DWORD gracefulShutdownTimeout; BOOL isClosing; long refCount; CNodeEventProvider* eventProvider; - HRESULT AddOneProcessCore(CNodeProcess** process, IHttpContext* context); - HRESULT AddOneProcess(CNodeProcess** process, IHttpContext* context); - BOOL TryRouteRequestToExistingProcess(CNodeHttpStoredContext* context); + HRESULT AddProcess(int ordinal, IHttpContext* context); static unsigned int WINAPI GracefulShutdown(void* arg); + void Cleanup(); public: diff --git a/src/samples/configuration/readme.htm b/src/samples/configuration/readme.htm index 1cb6d90..eebf7e0 100644 --- a/src/samples/configuration/readme.htm +++ b/src/samples/configuration/readme.htm @@ -54,12 +54,11 @@ console.log('Application started at location ' + process.env.PORT); * nodeProcessCommandLine - command line starting the node executable; in shared hosting environments this setting would typically be locked at the machine scope. - * maxProcessCountPerApplication - maximum number of node processes that IIS will start - per application to accomodate incresing request load + * nodeProcessCountPerApplication - number of node.exe processes that IIS will start per application; + setting this value to 0 results in creating one node.exe process per each processor on the machine * maxConcurrentRequestsPerProcess - maximum number of reqeusts one node process can - handle at a time; if a request arrives while all existing node processes have reached their - limit, a new node process is created to handle it (up to the limit of maxProcessCountPerApplication) + handle at a time * maxNamedPipeConnectionRetry - number of times IIS will retry to establish a named pipe connection with a node process in order to send a new HTTP request @@ -87,7 +86,7 @@ console.log('Application started at location ' + process.env.PORT); * logDirectoryNameSuffix - suffix of the directory name that will store files with stdout and stderr captures; directly name is created by appending this suffix to the file name of the node.js application; individual log files are stored in that directory, one per node - process (in files of the form x.txt, where x is between 0 and maxProcessCountPerApplication - 1); given a node.js application at + process (in files of the form x.txt, where x is between 0 and nodeProcessCountPerApplication - 1); given a node.js application at http://localhost/node/hello.js, log files would by default be stored at http://localhost/node/hello.js.logs/0.txt (thrugh 3.txt); SECURITY NOTE: if log files contain sensitive information, this setting should be modified to contain enough entropy to be considered cryptographically secure; in most situations, a GUID is sufficient @@ -119,7 +118,7 @@ console.log('Application started at location ' + process.env.PORT); <iisnode node_env="%node_env%" - maxProcessCountPerApplication="4" + nodeProcessCountPerApplication="1" maxConcurrentRequestsPerProcess="1024" maxNamedPipeConnectionRetry="3" namedPipeConnectionRetryDelay="2000" diff --git a/src/samples/configuration/web.config b/src/samples/configuration/web.config index 923064d..83934c6 100644 --- a/src/samples/configuration/web.config +++ b/src/samples/configuration/web.config @@ -20,12 +20,11 @@ * nodeProcessCommandLine - command line starting the node executable; in shared hosting environments this setting would typically be locked at the machine scope. - * maxProcessCountPerApplication - maximum number of node processes that IIS will start - per application to accomodate incresing request load + * nodeProcessCountPerApplication - number of node.exe processes that IIS will start per application; + setting this value to 0 results in creating one node.exe process per each processor on the machine * maxConcurrentRequestsPerProcess - maximum number of reqeusts one node process can - handle at a time; if a request arrives while all existing node processes have reached their - limit, a new node process is created to handle it (up to the limit of maxProcessCountPerApplication) + handle at a time * maxNamedPipeConnectionRetry - number of times IIS will retry to establish a named pipe connection with a node process in order to send a new HTTP request @@ -53,7 +52,7 @@ * logDirectoryNameSuffix - suffix of the directory name that will store files with stdout and stderr captures; directly name is created by appending this suffix to the file name of the node.js application; individual log files are stored in that directory, one per node - process (in files of the form x.txt, where x is between 0 and maxProcessCountPerApplication - 1); given a node.js application at + process (in files of the form x.txt, where x is between 0 and nodeProcessCountPerApplication - 1); given a node.js application at http://localhost/node/hello.js, log files would by default be stored at http://localhost/node/hello.js.logs/0.txt (thrugh 3.txt); SECURITY NOTE: if log files contain sensitive information, this setting should be modified to contain enough entropy to be considered cryptographically secure; in most situations, a GUID is sufficient @@ -85,7 +84,7 @@ + + + + + + + diff --git a/test/performance/www/cluster/server.js b/test/performance/www/cluster/server.js index e66d4e6..a7908ee 100644 --- a/test/performance/www/cluster/server.js +++ b/test/performance/www/cluster/server.js @@ -12,7 +12,7 @@ else { var cluster = require('cluster'); if (cluster.isMaster) { - for (var i = 0; i < 3; i++) { + for (var i = 0; i < 4; i++) { cluster.fork(); } diff --git a/test/performance/www/cluster/web.config b/test/performance/www/cluster/web.config index 265b757..a3d0e49 100644 --- a/test/performance/www/cluster/web.config +++ b/test/performance/www/cluster/web.config @@ -11,7 +11,7 @@ loggingEnabled="false" debuggingEnabled="false" devErrorsEnabled="false" - maxProcessCountPerApplication="3" + nodeProcessCountPerApplication="4" maxConcurrentRequestsPerProcess="999999999" maxNamedPipeConnectionRetry="10" /> diff --git a/test/performance/www/default/web.config b/test/performance/www/default/web.config index bd31a9c..99d2134 100644 --- a/test/performance/www/default/web.config +++ b/test/performance/www/default/web.config @@ -11,7 +11,7 @@ loggingEnabled="false" debuggingEnabled="false" devErrorsEnabled="false" - maxProcessCountPerApplication="1" + nodeProcessCountPerApplication="1" maxConcurrentRequestsPerProcess="999999999" maxNamedPipeConnectionRetry="10" />