Create new top-level DefaultNodeInstance concept that will soon hold the "connection draining" logic

This commit is contained in:
SteveSandersonMS 2016-07-06 18:23:25 +01:00
Родитель 4ee09cbe82
Коммит 4fb3b18868
19 изменённых файлов: 210 добавлений и 89 удалений

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

@ -12,21 +12,21 @@ namespace ConsoleApplication
public class Program
{
public static void Main(string[] args) {
using (var nodeServices = CreateNodeServices(Configuration.DefaultNodeHostingModel)) {
using (var nodeServices = CreateNodeServices(NodeServicesOptions.DefaultNodeHostingModel)) {
MeasureLatency(nodeServices).Wait();
}
}
private static async Task MeasureLatency(INodeServices nodeServices) {
// Ensure the connection is open, so we can measure per-request timings below
var response = await nodeServices.Invoke<string>("latencyTest", "C#");
var response = await nodeServices.InvokeAsync<string>("latencyTest", "C#");
Console.WriteLine(response);
// Now perform a series of requests, capturing the time taken
const int requestCount = 100;
var watch = Stopwatch.StartNew();
for (var i = 0; i < requestCount; i++) {
await nodeServices.Invoke<string>("latencyTest", "C#");
await nodeServices.InvokeAsync<string>("latencyTest", "C#");
}
// Display results

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

@ -46,7 +46,7 @@ namespace NodeServicesExamples.Controllers
}
// Invoke Node and pipe the result to the response
var imageStream = await _nodeServices.Invoke<Stream>(
var imageStream = await _nodeServices.InvokeAsync<Stream>(
"./Node/resizeImage",
fileInfo.PhysicalPath,
mimeType,

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

@ -30,7 +30,7 @@ namespace NodeServicesExamples
if (requestPath.StartsWith("/js/") && requestPath.EndsWith(".js")) {
var fileInfo = env.WebRootFileProvider.GetFileInfo(requestPath);
if (fileInfo.Exists) {
var transpiled = await nodeServices.Invoke<string>("./Node/transpilation.js", fileInfo.PhysicalPath, requestPath);
var transpiled = await nodeServices.InvokeAsync<string>("./Node/transpilation.js", fileInfo.PhysicalPath, requestPath);
await context.Response.WriteAsync(transpiled);
return;
}

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

@ -1,49 +0,0 @@
using System;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.AspNetCore.Hosting;
namespace Microsoft.AspNetCore.NodeServices
{
public static class Configuration
{
public const NodeHostingModel DefaultNodeHostingModel = NodeHostingModel.Http;
private static readonly string[] DefaultWatchFileExtensions = {".js", ".jsx", ".ts", ".tsx", ".json", ".html"};
private static readonly NodeServicesOptions DefaultOptions = new NodeServicesOptions
{
HostingModel = DefaultNodeHostingModel,
WatchFileExtensions = DefaultWatchFileExtensions
};
public static void AddNodeServices(this IServiceCollection serviceCollection)
=> AddNodeServices(serviceCollection, DefaultOptions);
public static void AddNodeServices(this IServiceCollection serviceCollection, NodeServicesOptions options)
{
serviceCollection.AddSingleton(typeof(INodeServices), serviceProvider =>
{
var hostEnv = serviceProvider.GetRequiredService<IHostingEnvironment>();
if (string.IsNullOrEmpty(options.ProjectPath))
{
options.ProjectPath = hostEnv.ContentRootPath;
}
return CreateNodeServices(options);
});
}
public static INodeServices CreateNodeServices(NodeServicesOptions options)
{
var watchFileExtensions = options.WatchFileExtensions ?? DefaultWatchFileExtensions;
switch (options.HostingModel)
{
case NodeHostingModel.Http:
return new HttpNodeInstance(options.ProjectPath, /* port */ 0, watchFileExtensions);
case NodeHostingModel.Socket:
return new SocketNodeInstance(options.ProjectPath, watchFileExtensions);
default:
throw new ArgumentException("Unknown hosting model: " + options.HostingModel);
}
}
}
}

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

@ -0,0 +1,58 @@
using System;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.NodeServices.HostingModels;
namespace Microsoft.AspNetCore.NodeServices
{
public static class Configuration
{
public static void AddNodeServices(this IServiceCollection serviceCollection)
=> AddNodeServices(serviceCollection, new NodeServicesOptions());
public static void AddNodeServices(this IServiceCollection serviceCollection, NodeServicesOptions options)
{
serviceCollection.AddSingleton(typeof(INodeServices), serviceProvider =>
{
// Since this instance is being created through DI, we can access the IHostingEnvironment
// to populate options.ProjectPath if it wasn't explicitly specified.
var hostEnv = serviceProvider.GetRequiredService<IHostingEnvironment>();
if (string.IsNullOrEmpty(options.ProjectPath))
{
options.ProjectPath = hostEnv.ContentRootPath;
}
return new NodeServicesImpl(options, () => CreateNodeInstance(options));
});
}
public static INodeServices CreateNodeServices(NodeServicesOptions options)
{
return new NodeServicesImpl(options, () => CreateNodeInstance(options));
}
private static INodeInstance CreateNodeInstance(NodeServicesOptions options)
{
if (options.NodeInstanceFactory != null)
{
// If you've explicitly supplied an INodeInstance factory, we'll use that. This is useful for
// custom INodeInstance implementations.
return options.NodeInstanceFactory();
}
else
{
// Otherwise we'll construct the type of INodeInstance specified by the HostingModel property,
// which itself has a useful default value.
switch (options.HostingModel)
{
case NodeHostingModel.Http:
return new HttpNodeInstance(options.ProjectPath, /* port */ 0, options.WatchFileExtensions);
case NodeHostingModel.Socket:
return new SocketNodeInstance(options.ProjectPath, options.WatchFileExtensions);
default:
throw new ArgumentException("Unknown hosting model: " + options.HostingModel);
}
}
}
}
}

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

@ -0,0 +1,23 @@
using System;
using Microsoft.AspNetCore.NodeServices.HostingModels;
namespace Microsoft.AspNetCore.NodeServices
{
public class NodeServicesOptions
{
public const NodeHostingModel DefaultNodeHostingModel = NodeHostingModel.Http;
private static readonly string[] DefaultWatchFileExtensions = { ".js", ".jsx", ".ts", ".tsx", ".json", ".html" };
public NodeServicesOptions()
{
HostingModel = DefaultNodeHostingModel;
WatchFileExtensions = (string[])DefaultWatchFileExtensions.Clone();
}
public NodeHostingModel HostingModel { get; set; }
public Func<INodeInstance> NodeInstanceFactory { get; set; }
public string ProjectPath { get; set; }
public string[] WatchFileExtensions { get; set; }
}
}

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

@ -7,7 +7,7 @@ using System.Threading.Tasks;
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
namespace Microsoft.AspNetCore.NodeServices
namespace Microsoft.AspNetCore.NodeServices.HostingModels
{
internal class HttpNodeInstance : OutOfProcessNodeInstance
{
@ -45,7 +45,7 @@ namespace Microsoft.AspNetCore.NodeServices
return result;
}
public override async Task<T> Invoke<T>(NodeInvocationInfo invocationInfo)
protected override async Task<T> InvokeExportAsync<T>(NodeInvocationInfo invocationInfo)
{
await EnsureReady();

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

@ -0,0 +1,10 @@
using System;
using System.Threading.Tasks;
namespace Microsoft.AspNetCore.NodeServices.HostingModels
{
public interface INodeInstance : IDisposable
{
Task<T> InvokeExportAsync<T>(string moduleName, string exportNameOrNull, params object[] args);
}
}

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

@ -1,6 +1,6 @@
using System;
namespace Microsoft.AspNetCore.NodeServices
namespace Microsoft.AspNetCore.NodeServices.HostingModels
{
public class NodeInvocationException : Exception
{

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

@ -1,4 +1,4 @@
namespace Microsoft.AspNetCore.NodeServices
namespace Microsoft.AspNetCore.NodeServices.HostingModels
{
public class NodeInvocationInfo
{

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

@ -3,14 +3,14 @@ using System.Diagnostics;
using System.IO;
using System.Threading.Tasks;
namespace Microsoft.AspNetCore.NodeServices
namespace Microsoft.AspNetCore.NodeServices.HostingModels
{
/// <summary>
/// Class responsible for launching the Node child process, determining when it is ready to accept invocations,
/// and finally killing it when the parent process exits. Also it restarts the child process if it dies.
/// </summary>
/// <seealso cref="Microsoft.AspNetCore.NodeServices.INodeServices" />
public abstract class OutOfProcessNodeInstance : INodeServices
/// <seealso cref="Microsoft.AspNetCore.NodeServices.INodeInstance" />
public abstract class OutOfProcessNodeInstance : INodeInstance
{
private readonly object _childProcessLauncherLock;
private string _commandLineArguments;
@ -34,15 +34,12 @@ namespace Microsoft.AspNetCore.NodeServices
set { _commandLineArguments = value; }
}
public Task<T> Invoke<T>(string moduleName, params object[] args)
=> InvokeExport<T>(moduleName, null, args);
public Task<T> InvokeExport<T>(string moduleName, string exportedFunctionName, params object[] args)
public Task<T> InvokeExportAsync<T>(string moduleName, string exportNameOrNull, params object[] args)
{
return Invoke<T>(new NodeInvocationInfo
return InvokeExportAsync<T>(new NodeInvocationInfo
{
ModuleName = moduleName,
ExportedFunctionName = exportedFunctionName,
ExportedFunctionName = exportNameOrNull,
Args = args
});
}
@ -53,7 +50,7 @@ namespace Microsoft.AspNetCore.NodeServices
GC.SuppressFinalize(this);
}
public abstract Task<T> Invoke<T>(NodeInvocationInfo invocationInfo);
protected abstract Task<T> InvokeExportAsync<T>(NodeInvocationInfo invocationInfo);
protected void ExitNodeProcess()
{

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

@ -8,7 +8,7 @@ using Microsoft.AspNetCore.NodeServices.HostingModels.VirtualConnections;
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
namespace Microsoft.AspNetCore.NodeServices
namespace Microsoft.AspNetCore.NodeServices.HostingModels
{
internal class SocketNodeInstance : OutOfProcessNodeInstance
{
@ -32,7 +32,7 @@ namespace Microsoft.AspNetCore.NodeServices
_watchFileExtensions = watchFileExtensions;
}
public override async Task<T> Invoke<T>(NodeInvocationInfo invocationInfo)
protected override async Task<T> InvokeExportAsync<T>(NodeInvocationInfo invocationInfo)
{
await EnsureReady();
var virtualConnectionClient = await GetOrCreateVirtualConnectionClientAsync();

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

@ -5,8 +5,14 @@ namespace Microsoft.AspNetCore.NodeServices
{
public interface INodeServices : IDisposable
{
Task<T> InvokeAsync<T>(string moduleName, params object[] args);
Task<T> InvokeExportAsync<T>(string moduleName, string exportedFunctionName, params object[] args);
[Obsolete("Use InvokeAsync instead")]
Task<T> Invoke<T>(string moduleName, params object[] args);
[Obsolete("Use InvokeExportAsync instead")]
Task<T> InvokeExport<T>(string moduleName, string exportedFunctionName, params object[] args);
}
}
}

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

@ -0,0 +1,92 @@
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.NodeServices.HostingModels;
namespace Microsoft.AspNetCore.NodeServices
{
/// <summary>
/// Default implementation of INodeServices. This is the primary API surface through which developers
/// make use of this package. It provides simple "InvokeAsync" methods that dispatch calls to the
/// correct Node instance, creating and destroying those instances as needed.
///
/// If a Node instance dies (or none was yet created), this class takes care of creating a new one.
/// If a Node instance signals that it needs to be restarted (e.g., because a file changed), then this
/// class will create a new instance and dispatch future calls to it, while keeping the old instance
/// alive for a defined period so that any in-flight RPC calls can complete. This latter feature is
/// analogous to the "connection draining" feature implemented by HTTP load balancers.
///
/// TODO: Implement everything in the preceding paragraph.
/// </summary>
/// <seealso cref="Microsoft.AspNetCore.NodeServices.INodeServices" />
internal class NodeServicesImpl : INodeServices
{
private NodeServicesOptions _options;
private Func<INodeInstance> _nodeInstanceFactory;
private INodeInstance _currentNodeInstance;
private object _currentNodeInstanceAccessLock = new object();
internal NodeServicesImpl(NodeServicesOptions options, Func<INodeInstance> nodeInstanceFactory)
{
_options = options;
_nodeInstanceFactory = nodeInstanceFactory;
}
public Task<T> InvokeAsync<T>(string moduleName, params object[] args)
{
return InvokeExportAsync<T>(moduleName, null, args);
}
public Task<T> InvokeExportAsync<T>(string moduleName, string exportedFunctionName, params object[] args)
{
var nodeInstance = GetOrCreateCurrentNodeInstance();
return nodeInstance.InvokeExportAsync<T>(moduleName, exportedFunctionName, args);
}
public void Dispose()
{
lock (_currentNodeInstanceAccessLock)
{
if (_currentNodeInstance != null)
{
_currentNodeInstance.Dispose();
_currentNodeInstance = null;
}
}
}
private INodeInstance GetOrCreateCurrentNodeInstance()
{
var instance = _currentNodeInstance;
if (instance == null)
{
lock (_currentNodeInstanceAccessLock)
{
instance = _currentNodeInstance;
if (instance == null)
{
instance = _currentNodeInstance = CreateNewNodeInstance();
}
}
}
return instance;
}
private INodeInstance CreateNewNodeInstance()
{
return _nodeInstanceFactory();
}
// Obsolete method - will be removed soon
public Task<T> Invoke<T>(string moduleName, params object[] args)
{
return InvokeAsync<T>(moduleName, args);
}
// Obsolete method - will be removed soon
public Task<T> InvokeExport<T>(string moduleName, string exportedFunctionName, params object[] args)
{
return InvokeExportAsync<T>(moduleName, exportedFunctionName, args);
}
}
}

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

@ -1,14 +0,0 @@
namespace Microsoft.AspNetCore.NodeServices
{
public class NodeServicesOptions
{
public NodeServicesOptions()
{
HostingModel = Configuration.DefaultNodeHostingModel;
}
public NodeHostingModel HostingModel { get; set; }
public string ProjectPath { get; set; }
public string[] WatchFileExtensions { get; set; }
}
}

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

@ -37,7 +37,6 @@ namespace Microsoft.AspNetCore.SpaServices.Prerendering
{
_nodeServices = _fallbackNodeServices = Configuration.CreateNodeServices(new NodeServicesOptions
{
HostingModel = Configuration.DefaultNodeHostingModel,
ProjectPath = _applicationBasePath
});
}

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

@ -25,7 +25,7 @@ namespace Microsoft.AspNetCore.SpaServices.Prerendering
string requestPathAndQuery,
object customDataParameter)
{
return nodeServices.InvokeExport<RenderToStringResult>(
return nodeServices.InvokeExportAsync<RenderToStringResult>(
NodeScript.Value.FileName,
"renderToString",
applicationBasePath,

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

@ -44,7 +44,6 @@ namespace Microsoft.AspNetCore.Builder
var hostEnv = (IHostingEnvironment)appBuilder.ApplicationServices.GetService(typeof(IHostingEnvironment));
var nodeServices = Configuration.CreateNodeServices(new NodeServicesOptions
{
HostingModel = Configuration.DefaultNodeHostingModel,
ProjectPath = hostEnv.ContentRootPath,
WatchFileExtensions = new string[] { } // Don't watch anything
});
@ -61,7 +60,7 @@ namespace Microsoft.AspNetCore.Builder
suppliedOptions = options
};
var devServerInfo =
nodeServices.InvokeExport<WebpackDevServerInfo>(nodeScript.FileName, "createWebpackDevServer",
nodeServices.InvokeExportAsync<WebpackDevServerInfo>(nodeScript.FileName, "createWebpackDevServer",
JsonConvert.SerializeObject(devServerOptions)).Result;
// Proxy the corresponding requests through ASP.NET and into the Node listener