fix #129: simplify support for multiple node processes

This commit is contained in:
Tomasz Janczuk 2011-12-21 12:30:11 -08:00
Родитель cfefd0a6ff
Коммит 982c518cf6
14 изменённых файлов: 140 добавлений и 117 удалений

Просмотреть файл

@ -29,7 +29,7 @@ Details at http://learn.iis.net/page.aspx/241/configuration-extensibility/
<sectionSchema name="system.webServer/iisnode">
<attribute name="node_env" type="string" expanded="true" defaultValue="%node_env%"/>
<attribute name="asyncCompletionThreadCount" type="uint" defaultValue="0"/>
<attribute name="maxProcessCountPerApplication" type="uint" defaultValue="4"/>
<attribute name="nodeProcessCountPerApplication" type="uint" defaultValue="1"/>
<attribute name="nodeProcessCommandLine" type="string" expanded="true" defaultValue="&quot;%programfiles%\nodejs\node.exe&quot;"/>
<attribute name="maxConcurrentRequestsPerProcess" type="uint" allowInfitnite="true" defaultValue="1024"/>
<attribute name="maxNamedPipeConnectionRetry" type="uint" defaultValue="3"/>

Просмотреть файл

@ -29,7 +29,7 @@ Details at http://learn.iis.net/page.aspx/241/configuration-extensibility/
<sectionSchema name="system.webServer/iisnode">
<attribute name="node_env" type="string" expanded="true" defaultValue="%node_env%"/>
<attribute name="asyncCompletionThreadCount" type="uint" defaultValue="0"/>
<attribute name="maxProcessCountPerApplication" type="uint" defaultValue="4"/>
<attribute name="nodeProcessCountPerApplication" type="uint" defaultValue="1"/>
<attribute name="nodeProcessCommandLine" type="string" expanded="true" defaultValue="&quot;%programfiles(x86)%\nodejs\node.exe&quot;"/>
<attribute name="maxConcurrentRequestsPerProcess" type="uint" allowInfitnite="true" defaultValue="1024"/>
<attribute name="maxNamedPipeConnectionRetry" type="uint" defaultValue="3"/>

Просмотреть файл

@ -434,7 +434,7 @@ HRESULT CModuleConfiguration::GetConfig(IHttpContext* context, CModuleConfigurat
CheckError(GetConfigSection(context, &section));
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)

Просмотреть файл

@ -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);

Просмотреть файл

@ -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]);
}

Просмотреть файл

@ -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:

Просмотреть файл

@ -54,12 +54,11 @@ console.log('Application started at location ' + process.env.PORT);</pre>
* 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);</pre>
* 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);</pre>
&lt;iisnode
node_env="%node_env%"
maxProcessCountPerApplication="4"
nodeProcessCountPerApplication="1"
maxConcurrentRequestsPerProcess="1024"
maxNamedPipeConnectionRetry="3"
namedPipeConnectionRetryDelay="2000"

Просмотреть файл

@ -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 @@
<iisnode
node_env="%node_env%"
maxProcessCountPerApplication="4"
nodeProcessCountPerApplication="1"
maxConcurrentRequestsPerProcess="1024"
maxNamedPipeConnectionRetry="3"
namedPipeConnectionRetryDelay="2000"

Просмотреть файл

@ -0,0 +1,12 @@
/*
Four consecutive requests are dispatched to two node.exe processes following a round robin algorithm
*/
var iisnodeassert = require("iisnodeassert");
iisnodeassert.sequence([
iisnodeassert.get(10000, "/114_roundrobin/hello.js", 200, "Hello, world 1"),
iisnodeassert.get(2000, "/114_roundrobin/hello.js", 200, "Hello, world 1"),
iisnodeassert.get(2000, "/114_roundrobin/hello.js", 200, "Hello, world 2"),
iisnodeassert.get(2000, "/114_roundrobin/hello.js", 200, "Hello, world 2")
]);

Просмотреть файл

@ -0,0 +1,8 @@
var http = require('http');
var n = 1;
http.createServer(function (req, res) {
res.writeHead(200, {'Content-Type': 'text/html'});
res.end('Hello, world ' + n++);
}).listen(process.env.PORT);

Просмотреть файл

@ -0,0 +1,8 @@
<configuration>
<system.webServer>
<handlers>
<add name="iisnode" path="hello.js" verb="*" modules="iisnode" />
</handlers>
<iisnode nodeProcessCountPerApplication="2" />
</system.webServer>
</configuration>

Просмотреть файл

@ -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();
}

Просмотреть файл

@ -11,7 +11,7 @@
loggingEnabled="false"
debuggingEnabled="false"
devErrorsEnabled="false"
maxProcessCountPerApplication="3"
nodeProcessCountPerApplication="4"
maxConcurrentRequestsPerProcess="999999999"
maxNamedPipeConnectionRetry="10"
/>

Просмотреть файл

@ -11,7 +11,7 @@
loggingEnabled="false"
debuggingEnabled="false"
devErrorsEnabled="false"
maxProcessCountPerApplication="1"
nodeProcessCountPerApplication="1"
maxConcurrentRequestsPerProcess="999999999"
maxNamedPipeConnectionRetry="10" />
<urlCompression doStaticCompression="false" doDynamicCompression="false" />