Create new top-level DefaultNodeInstance concept that will soon hold the "connection draining" logic
This commit is contained in:
Родитель
4ee09cbe82
Коммит
4fb3b18868
|
@ -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
|
||||
|
|
Загрузка…
Ссылка в новой задаче