All NodeServices invocations now have a default timeout, plus a descriptive exception if that happens

This commit is contained in:
SteveSandersonMS
2016-09-08 12:08:42 +01:00
parent 2799861296
commit 041d173f56
5 changed files with 81 additions and 14 deletions

View File

@@ -29,11 +29,11 @@ namespace Microsoft.AspNetCore.NodeServices
{ {
case NodeHostingModel.Http: case NodeHostingModel.Http:
return new HttpNodeInstance(options.ProjectPath, options.WatchFileExtensions, options.NodeInstanceOutputLogger, return new HttpNodeInstance(options.ProjectPath, options.WatchFileExtensions, options.NodeInstanceOutputLogger,
options.EnvironmentVariables, options.LaunchWithDebugging, options.DebuggingPort, /* port */ 0); options.EnvironmentVariables, options.InvocationTimeoutMilliseconds, options.LaunchWithDebugging, options.DebuggingPort, /* port */ 0);
case NodeHostingModel.Socket: case NodeHostingModel.Socket:
var pipeName = "pni-" + Guid.NewGuid().ToString("D"); // Arbitrary non-clashing string var pipeName = "pni-" + Guid.NewGuid().ToString("D"); // Arbitrary non-clashing string
return new SocketNodeInstance(options.ProjectPath, options.WatchFileExtensions, pipeName, options.NodeInstanceOutputLogger, return new SocketNodeInstance(options.ProjectPath, options.WatchFileExtensions, pipeName, options.NodeInstanceOutputLogger,
options.EnvironmentVariables, options.LaunchWithDebugging, options.DebuggingPort); options.EnvironmentVariables, options.InvocationTimeoutMilliseconds, options.LaunchWithDebugging, options.DebuggingPort);
default: default:
throw new ArgumentException("Unknown hosting model: " + options.HostingModel); throw new ArgumentException("Unknown hosting model: " + options.HostingModel);
} }

View File

@@ -11,6 +11,8 @@ namespace Microsoft.AspNetCore.NodeServices
public class NodeServicesOptions public class NodeServicesOptions
{ {
public const NodeHostingModel DefaultNodeHostingModel = NodeHostingModel.Http; public const NodeHostingModel DefaultNodeHostingModel = NodeHostingModel.Http;
internal const string TimeoutConfigPropertyName = nameof(InvocationTimeoutMilliseconds);
private const int DefaultInvocationTimeoutMilliseconds = 60 * 1000;
private const string LogCategoryName = "Microsoft.AspNetCore.NodeServices"; private const string LogCategoryName = "Microsoft.AspNetCore.NodeServices";
private static readonly string[] DefaultWatchFileExtensions = { ".js", ".jsx", ".ts", ".tsx", ".json", ".html" }; private static readonly string[] DefaultWatchFileExtensions = { ".js", ".jsx", ".ts", ".tsx", ".json", ".html" };
@@ -22,6 +24,7 @@ namespace Microsoft.AspNetCore.NodeServices
} }
EnvironmentVariables = new Dictionary<string, string>(); EnvironmentVariables = new Dictionary<string, string>();
InvocationTimeoutMilliseconds = DefaultInvocationTimeoutMilliseconds;
HostingModel = DefaultNodeHostingModel; HostingModel = DefaultNodeHostingModel;
WatchFileExtensions = (string[])DefaultWatchFileExtensions.Clone(); WatchFileExtensions = (string[])DefaultWatchFileExtensions.Clone();
@@ -49,5 +52,6 @@ namespace Microsoft.AspNetCore.NodeServices
public bool LaunchWithDebugging { get; set; } public bool LaunchWithDebugging { get; set; }
public IDictionary<string, string> EnvironmentVariables { get; set; } public IDictionary<string, string> EnvironmentVariables { get; set; }
public int DebuggingPort { get; set; } public int DebuggingPort { get; set; }
public int InvocationTimeoutMilliseconds { get; set; }
} }
} }

View File

@@ -37,7 +37,8 @@ namespace Microsoft.AspNetCore.NodeServices.HostingModels
private int _portNumber; private int _portNumber;
public HttpNodeInstance(string projectPath, string[] watchFileExtensions, ILogger nodeInstanceOutputLogger, public HttpNodeInstance(string projectPath, string[] watchFileExtensions, ILogger nodeInstanceOutputLogger,
IDictionary<string, string> environmentVars, bool launchWithDebugging, int debuggingPort, int port = 0) IDictionary<string, string> environmentVars, int invocationTimeoutMilliseconds, bool launchWithDebugging,
int debuggingPort, int port = 0)
: base( : base(
EmbeddedResourceReader.Read( EmbeddedResourceReader.Read(
typeof(HttpNodeInstance), typeof(HttpNodeInstance),
@@ -47,6 +48,7 @@ namespace Microsoft.AspNetCore.NodeServices.HostingModels
MakeCommandLineOptions(port), MakeCommandLineOptions(port),
nodeInstanceOutputLogger, nodeInstanceOutputLogger,
environmentVars, environmentVars,
invocationTimeoutMilliseconds,
launchWithDebugging, launchWithDebugging,
debuggingPort) debuggingPort)
{ {

View File

@@ -37,6 +37,7 @@ If you haven't yet installed node-inspector, you can do so as follows:
private bool _disposed; private bool _disposed;
private readonly StringAsTempFile _entryPointScript; private readonly StringAsTempFile _entryPointScript;
private FileSystemWatcher _fileSystemWatcher; private FileSystemWatcher _fileSystemWatcher;
private int _invocationTimeoutMilliseconds;
private readonly Process _nodeProcess; private readonly Process _nodeProcess;
private int? _nodeDebuggingPort; private int? _nodeDebuggingPort;
private bool _nodeProcessNeedsRestart; private bool _nodeProcessNeedsRestart;
@@ -49,6 +50,7 @@ If you haven't yet installed node-inspector, you can do so as follows:
string commandLineArguments, string commandLineArguments,
ILogger nodeOutputLogger, ILogger nodeOutputLogger,
IDictionary<string, string> environmentVars, IDictionary<string, string> environmentVars,
int invocationTimeoutMilliseconds,
bool launchWithDebugging, bool launchWithDebugging,
int debuggingPort) int debuggingPort)
{ {
@@ -59,6 +61,7 @@ If you haven't yet installed node-inspector, you can do so as follows:
OutputLogger = nodeOutputLogger; OutputLogger = nodeOutputLogger;
_entryPointScript = new StringAsTempFile(entryPointScript); _entryPointScript = new StringAsTempFile(entryPointScript);
_invocationTimeoutMilliseconds = invocationTimeoutMilliseconds;
var startInfo = PrepareNodeProcessStartInfo(_entryPointScript.FileName, projectPath, commandLineArguments, var startInfo = PrepareNodeProcessStartInfo(_entryPointScript.FileName, projectPath, commandLineArguments,
environmentVars, launchWithDebugging, debuggingPort); environmentVars, launchWithDebugging, debuggingPort);
@@ -81,10 +84,29 @@ If you haven't yet installed node-inspector, you can do so as follows:
throw new NodeInvocationException(message, null, nodeInstanceUnavailable: true); throw new NodeInvocationException(message, null, nodeInstanceUnavailable: true);
} }
// Construct a new cancellation token that combines the supplied token with the configured invocation
// timeout. Technically we could avoid wrapping the cancellationToken if no timeout is configured,
// but that's not really a major use case, since timeouts are enabled by default.
using (var timeoutSource = new CancellationTokenSource())
using (var combinedCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutSource.Token))
{
if (_invocationTimeoutMilliseconds > 0)
{
timeoutSource.CancelAfter(_invocationTimeoutMilliseconds);
}
// By overwriting the supplied cancellation token, we ensure that it isn't accidentally used
// below. We only want to pass through the token that respects timeouts.
cancellationToken = combinedCancellationTokenSource.Token;
var connectionDidSucceed = false;
try
{
// Wait until the connection is established. This will throw if the connection fails to initialize, // Wait until the connection is established. This will throw if the connection fails to initialize,
// or if cancellation is requested first. Note that we can't really cancel the "establishing connection" // or if cancellation is requested first. Note that we can't really cancel the "establishing connection"
// task because that's shared with all callers, but we can stop waiting for it if this call is cancelled. // task because that's shared with all callers, but we can stop waiting for it if this call is cancelled.
await _connectionIsReadySource.Task.OrThrowOnCancellation(cancellationToken); await _connectionIsReadySource.Task.OrThrowOnCancellation(cancellationToken);
connectionDidSucceed = true;
return await InvokeExportAsync<T>(new NodeInvocationInfo return await InvokeExportAsync<T>(new NodeInvocationInfo
{ {
@@ -93,6 +115,44 @@ If you haven't yet installed node-inspector, you can do so as follows:
Args = args Args = args
}, cancellationToken); }, cancellationToken);
} }
catch (TaskCanceledException)
{
if (timeoutSource.IsCancellationRequested)
{
// It was very common for developers to report 'TaskCanceledException' when encountering almost any
// trouble when using NodeServices. Now we have a default invocation timeout, and attempt to give
// a more descriptive exception message if it happens.
if (!connectionDidSucceed)
{
// This is very unlikely, but for debugging, it's still useful to differentiate it from the
// case below.
throw new NodeInvocationException(
$"Attempt to connect to Node timed out after {_invocationTimeoutMilliseconds}ms.",
string.Empty);
}
else
{
// Developers encounter this fairly often (if their Node code fails without invoking the callback,
// all that the .NET side knows is that the invocation eventually times out). Previously, this surfaced
// as a TaskCanceledException, but this led to a lot of issue reports. Now we throw the following
// descriptive error.
throw new NodeInvocationException(
$"The Node invocation timed out after {_invocationTimeoutMilliseconds}ms.",
$"You can change the timeout duration by setting the {NodeServicesOptions.TimeoutConfigPropertyName} "
+ $"property on {nameof(NodeServicesOptions)}.\n\n"
+ "The first debugging step is to ensure that your Node.js function always invokes the supplied "
+ "callback (or throws an exception synchronously), even if it encounters an error. Otherwise, "
+ "the .NET code has no way to know that it is finished or has failed."
);
}
}
else
{
throw;
}
}
}
}
public void Dispose() public void Dispose()
{ {

View File

@@ -41,7 +41,7 @@ namespace Microsoft.AspNetCore.NodeServices.HostingModels
public SocketNodeInstance(string projectPath, string[] watchFileExtensions, string socketAddress, public SocketNodeInstance(string projectPath, string[] watchFileExtensions, string socketAddress,
ILogger nodeInstanceOutputLogger, IDictionary<string, string> environmentVars, ILogger nodeInstanceOutputLogger, IDictionary<string, string> environmentVars,
bool launchWithDebugging, int debuggingPort) int invocationTimeoutMilliseconds, bool launchWithDebugging, int debuggingPort)
: base( : base(
EmbeddedResourceReader.Read( EmbeddedResourceReader.Read(
typeof(SocketNodeInstance), typeof(SocketNodeInstance),
@@ -51,6 +51,7 @@ namespace Microsoft.AspNetCore.NodeServices.HostingModels
MakeNewCommandLineOptions(socketAddress), MakeNewCommandLineOptions(socketAddress),
nodeInstanceOutputLogger, nodeInstanceOutputLogger,
environmentVars, environmentVars,
invocationTimeoutMilliseconds,
launchWithDebugging, launchWithDebugging,
debuggingPort) debuggingPort)
{ {