using System; using System.Threading.Tasks; using Microsoft.AspNetCore.NodeServices.HostingModels; namespace Microsoft.AspNetCore.NodeServices { /// /// 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. /// /// internal class NodeServicesImpl : INodeServices { private NodeServicesOptions _options; private Func _nodeInstanceFactory; private INodeInstance _currentNodeInstance; private object _currentNodeInstanceAccessLock = new object(); internal NodeServicesImpl(NodeServicesOptions options, Func nodeInstanceFactory) { _options = options; _nodeInstanceFactory = nodeInstanceFactory; } public Task InvokeAsync(string moduleName, params object[] args) { return InvokeExportAsync(moduleName, null, args); } public Task InvokeExportAsync(string moduleName, string exportedFunctionName, params object[] args) { return InvokeExportWithPossibleRetryAsync(moduleName, exportedFunctionName, args, allowRetry: true); } public async Task InvokeExportWithPossibleRetryAsync(string moduleName, string exportedFunctionName, object[] args, bool allowRetry) { var nodeInstance = GetOrCreateCurrentNodeInstance(); try { return await nodeInstance.InvokeExportAsync(moduleName, exportedFunctionName, args); } catch (NodeInvocationException ex) { // If the Node instance can't complete the invocation because it needs to restart (e.g., because the underlying // Node process has exited, or a file it depends on has changed), then we make one attempt to restart transparently. if (allowRetry && ex.NodeInstanceUnavailable) { // Perform the retry after clearing away the old instance lock (_currentNodeInstanceAccessLock) { if (_currentNodeInstance == nodeInstance) { DisposeNodeInstance(_currentNodeInstance); _currentNodeInstance = null; } } // One the next call, don't allow retries, because we could get into an infinite retry loop, or a long retry // loop that masks an underlying problem. A newly-created Node instance should be able to accept invocations, // or something more serious must be wrong. return await InvokeExportWithPossibleRetryAsync(moduleName, exportedFunctionName, args, allowRetry: false); } else { throw; } } } public void Dispose() { lock (_currentNodeInstanceAccessLock) { if (_currentNodeInstance != null) { DisposeNodeInstance(_currentNodeInstance); _currentNodeInstance = null; } } } private static void DisposeNodeInstance(INodeInstance nodeInstance) { // TODO: Implement delayed disposal for connection draining // Or consider having the delayedness of it being a responsibility of the INodeInstance nodeInstance.Dispose(); } 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 Invoke(string moduleName, params object[] args) { return InvokeAsync(moduleName, args); } // Obsolete method - will be removed soon public Task InvokeExport(string moduleName, string exportedFunctionName, params object[] args) { return InvokeExportAsync(moduleName, exportedFunctionName, args); } } }