Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New programming model #849

Open
wants to merge 4 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions src/DependencyManagement/DependencyManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ internal class DependencyManager : IDisposable
#endregion

public DependencyManager(
string requestMetadataDirectory = null,
string functionAppRootPath = null,
IModuleProvider moduleProvider = null,
IDependencyManagerStorage storage = null,
IInstalledDependenciesLocator installedDependenciesLocator = null,
Expand All @@ -54,7 +54,7 @@ public DependencyManager(
IBackgroundDependencySnapshotContentLogger currentSnapshotContentLogger = null,
ILogger logger = null)
{
_storage = storage ?? new DependencyManagerStorage(GetFunctionAppRootPath(requestMetadataDirectory));
_storage = storage ?? new DependencyManagerStorage(GetFunctionAppRootPath(functionAppRootPath));
_installedDependenciesLocator = installedDependenciesLocator ?? new InstalledDependenciesLocator(_storage, logger);
var snapshotContentLogger = new PowerShellModuleSnapshotLogger();
_installer = installer ?? new DependencySnapshotInstaller(
Expand Down Expand Up @@ -252,14 +252,14 @@ private bool AreAcceptableDependenciesAlreadyInstalled()
return _storage.SnapshotExists(_currentSnapshotPath);
}

private static string GetFunctionAppRootPath(string requestMetadataDirectory)
private static string GetFunctionAppRootPath(string functionAppRootPath)
{
if (string.IsNullOrWhiteSpace(requestMetadataDirectory))
if (string.IsNullOrWhiteSpace(functionAppRootPath))
{
throw new ArgumentException("Empty request metadata directory path", nameof(requestMetadataDirectory));
throw new ArgumentException("Empty function app root path", nameof(functionAppRootPath));
}

return Path.GetFullPath(Path.Join(requestMetadataDirectory, ".."));
return functionAppRootPath;
}

#endregion
Expand Down
63 changes: 53 additions & 10 deletions src/RequestProcessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,10 @@

namespace Microsoft.Azure.Functions.PowerShellWorker
{
using Microsoft.Azure.Functions.PowerShellWorker.WorkerIndexing;
using Microsoft.PowerShell;
using System.Diagnostics;
using System.Text.Json;
using LogLevel = Microsoft.Azure.WebJobs.Script.Grpc.Messages.RpcLog.Types.Level;

internal class RequestProcessor
Expand Down Expand Up @@ -66,6 +69,8 @@ internal RequestProcessor(MessagingStream msgStream, System.Management.Automatio
// If an invocation is cancelled, host will receive an invocation response with status cancelled.
_requestHandlers.Add(StreamingMessage.ContentOneofCase.InvocationCancel, ProcessInvocationCancelRequest);

_requestHandlers.Add(StreamingMessage.ContentOneofCase.FunctionsMetadataRequest, ProcessFunctionMetadataRequest);

_requestHandlers.Add(StreamingMessage.ContentOneofCase.FunctionEnvironmentReloadRequest, ProcessFunctionEnvironmentReloadRequest);
}

Expand Down Expand Up @@ -95,6 +100,9 @@ internal async Task ProcessRequestLoop()

internal StreamingMessage ProcessWorkerInitRequest(StreamingMessage request)
{
var stopwatch = new Stopwatch();
stopwatch.Start();

var workerInitRequest = request.WorkerInitRequest;
Environment.SetEnvironmentVariable("AZUREPS_HOST_ENVIRONMENT", $"AzureFunctions/{workerInitRequest.HostVersion}");
Environment.SetEnvironmentVariable("POWERSHELL_DISTRIBUTION_CHANNEL", $"Azure-Functions:{workerInitRequest.HostVersion}");
Expand All @@ -117,6 +125,32 @@ internal StreamingMessage ProcessWorkerInitRequest(StreamingMessage request)
RemoteSessionNamedPipeServer.CreateCustomNamedPipeServer(pipeName);
}

// Previously, this half of the dependency management would happen just prior to the dependency download in the
// first function load request. Now that we have the FunctionAppDirectory in the WorkerInitRequest,
// we can do the setup of these variables in the function load request. We need these variables initialized
// for the FunctionMetadataRequest, should it be sent.
try
{
Francisco-Gamino marked this conversation as resolved.
Show resolved Hide resolved
var rpcLogger = new RpcLogger(_msgStream);
rpcLogger.SetContext(request.RequestId, null);

_dependencyManager = new DependencyManager(request.WorkerInitRequest.FunctionAppDirectory, logger: rpcLogger);

_powershellPool.Initialize(_firstPwshInstance);

rpcLogger.Log(isUserOnlyLog: false, LogLevel.Trace, string.Format(PowerShellWorkerStrings.FirstFunctionLoadCompleted, stopwatch.ElapsedMilliseconds));
}
Francisco-Gamino marked this conversation as resolved.
Show resolved Hide resolved
catch (Exception e)
{
// This is a terminating failure: we will need to return a failure response to
// all subsequent 'FunctionLoadRequest'. Cache the exception so we can reuse it in future calls.
_initTerminatingError = e;

status.Status = StatusResult.Types.Status.Failure;
status.Exception = e.ToRpcException();
return response;
}

return response;
}

Expand Down Expand Up @@ -189,26 +223,20 @@ internal StreamingMessage ProcessFunctionLoadRequest(StreamingMessage request)
{
try
{
_isFunctionAppInitialized = true;

var rpcLogger = new RpcLogger(_msgStream);
rpcLogger.SetContext(request.RequestId, null);

_dependencyManager = new DependencyManager(request.FunctionLoadRequest.Metadata.Directory, logger: rpcLogger);
var managedDependenciesPath = _dependencyManager.Initialize(request, rpcLogger);

SetupAppRootPathAndModulePath(functionLoadRequest, managedDependenciesPath);
_isFunctionAppInitialized = true;

_powershellPool.Initialize(_firstPwshInstance);
var managedDependenciesPath = _dependencyManager.Initialize(request, rpcLogger);

SetupAppRootPathAndModulePath(request.FunctionLoadRequest, managedDependenciesPath);
// Start the download asynchronously if needed.
_dependencyManager.StartDependencyInstallationIfNeeded(request, _firstPwshInstance, rpcLogger);

rpcLogger.Log(isUserOnlyLog: false, LogLevel.Trace, string.Format(PowerShellWorkerStrings.FirstFunctionLoadCompleted, stopwatch.ElapsedMilliseconds));
}
catch (Exception e)
{
// Failure that happens during this step is terminating and we will need to return a failure response to
// This is a terminating failure: we will need to return a failure response to
// all subsequent 'FunctionLoadRequest'. Cache the exception so we can reuse it in future calls.
_initTerminatingError = e;

Expand Down Expand Up @@ -341,6 +369,18 @@ internal StreamingMessage ProcessInvocationCancelRequest(StreamingMessage reques
return null;
}

private StreamingMessage ProcessFunctionMetadataRequest(StreamingMessage request)
{
StreamingMessage response = NewStreamingMessageTemplate(
request.RequestId,
StreamingMessage.ContentOneofCase.FunctionMetadataResponse,
out StatusResult status);

response.FunctionMetadataResponse.FunctionMetadataResults.AddRange(WorkerIndexingHelper.IndexFunctions(request.FunctionsMetadataRequest.FunctionAppDirectory));

return response;
}

internal StreamingMessage ProcessFunctionEnvironmentReloadRequest(StreamingMessage request)
{
var stopwatch = new Stopwatch();
Expand Down Expand Up @@ -394,6 +434,9 @@ private StreamingMessage NewStreamingMessageTemplate(string requestId, Streaming
case StreamingMessage.ContentOneofCase.FunctionEnvironmentReloadResponse:
response.FunctionEnvironmentReloadResponse = new FunctionEnvironmentReloadResponse() { Result = status };
break;
case StreamingMessage.ContentOneofCase.FunctionMetadataResponse:
response.FunctionMetadataResponse = new FunctionMetadataResponse() { Result = status };
break;
default:
throw new InvalidOperationException("Unreachable code.");
}
Expand Down
59 changes: 59 additions & 0 deletions src/WorkerIndexing/BindingInformation.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//

using Microsoft.Azure.WebJobs.Script.Grpc.Messages;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;

namespace Microsoft.Azure.Functions.PowerShellWorker.WorkerIndexing
{
internal class BindingInformation
{
private const string BindingNameKey = "name";
private const string BindingDirectionKey = "direction";
private const string BindingTypeKey = "type";
public enum Directions
{
Unknown = -1,
In = 0,
Out = 1,
Inout = 2
}

public Directions Direction { get; set; } = Directions.Unknown;
public string Type { get; set; } = "";
public string Name { get; set; } = "";
public Dictionary<string, Object> otherInformation { get; set; } = new Dictionary<string, Object>();

internal string ConvertToRpcRawBinding(out BindingInfo bindingInfo)
{
string rawBinding = string.Empty;
JObject rawBindingObject = new JObject();
rawBindingObject.Add(BindingNameKey, Name);
BindingInfo outInfo = new BindingInfo();


if (Direction == Directions.Unknown)
{
throw new Exception(string.Format(PowerShellWorkerStrings.InvalidBindingInfoDirection, Name));
}
outInfo.Direction = (BindingInfo.Types.Direction)Direction;
rawBindingObject.Add(BindingDirectionKey, Enum.GetName(typeof(BindingInfo.Types.Direction), outInfo.Direction).ToLower());
outInfo.Type = Type;
rawBindingObject.Add(BindingTypeKey, Type);

foreach (KeyValuePair<string, Object> pair in otherInformation)
{
rawBindingObject.Add(pair.Key, JToken.FromObject(pair.Value));
}

rawBinding = JsonConvert.SerializeObject(rawBindingObject);
bindingInfo = outInfo;
return rawBinding;
}
}
}
40 changes: 40 additions & 0 deletions src/WorkerIndexing/FunctionInformation.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//

using Microsoft.Azure.WebJobs.Script.Grpc.Messages;
using System.Collections.Generic;

namespace Microsoft.Azure.Functions.PowerShellWorker.WorkerIndexing
{
internal class FunctionInformation
{
private const string FunctionLanguagePowerShell = "powershell";

public string Directory { get; set; } = "";
public string ScriptFile { get; set; } = "";
public string Name { get; set; } = "";
public string EntryPoint { get; set; } = "";
public string FunctionId { get; set; } = "";
public List<BindingInformation> Bindings { get; set; } = new List<BindingInformation>();

internal RpcFunctionMetadata ConvertToRpc()
{
RpcFunctionMetadata returnMetadata = new RpcFunctionMetadata();
returnMetadata.FunctionId = FunctionId;
returnMetadata.Directory = Directory;
returnMetadata.EntryPoint = EntryPoint;
returnMetadata.Name = Name;
returnMetadata.ScriptFile = ScriptFile;
returnMetadata.Language = FunctionLanguagePowerShell;
foreach(BindingInformation binding in Bindings)
{
string rawBinding = binding.ConvertToRpcRawBinding(out BindingInfo bindingInfo);
returnMetadata.Bindings.Add(binding.Name, bindingInfo);
returnMetadata.RawBindings.Add(rawBinding);
}
return returnMetadata;
}
}
}
73 changes: 73 additions & 0 deletions src/WorkerIndexing/WorkerIndexingHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//

using Microsoft.Azure.WebJobs.Script.Grpc.Messages;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Management.Automation;
using System.Management.Automation.Runspaces;

namespace Microsoft.Azure.Functions.PowerShellWorker.WorkerIndexing
{
internal class WorkerIndexingHelper
{
// TODO: Follow up with the PowerShell on why we get a CommandNotFoundException when using the module qualified cmdlet name.
//const string GetFunctionsMetadataCmdletName = "AzureFunctions.PowerShell.SDK\\Get-FunctionsMetadata";
const string GetFunctionsMetadataCmdletName = "Get-FunctionsMetadata";
Francisco-Gamino marked this conversation as resolved.
Show resolved Hide resolved
internal static IEnumerable<RpcFunctionMetadata> IndexFunctions(string baseDir)
{
List<RpcFunctionMetadata> indexedFunctions = new List<RpcFunctionMetadata>();

// This is not the correct way to deal with getting a runspace for the cmdlet.

// Firstly, creating a runspace is expensive. If we are going to generate a runspace, it should be done on
// the function load request so that it can be created while the host is processing.

// Secondly, this assumes that the AzureFunctions.PowerShell.SDK module is present on the machine/VM's
// PSModulePath. On an Azure instance, it will not be. What we need to do here is move the call
// to SetupAppRootPathAndModulePath in RequestProcessor to the init request, and then use the
// _firstPwshInstance to invoke the Get-FunctionsMetadata command. The only issue with this is that
// SetupAppRootPathAndModulePath needs the initial function init request in order to know if managed
// dependencies are enabled in this function app.

// Proposed solutions:
// 1. Pass ManagedDependencyEnabled flag in the worker init request
// 2. Change the flow, so that _firstPwshInstance is initialized in worker init with the PSModulePath
// assuming that managed dependencies are enabled, and then revert the PSModulePath in the first function
// init request should the managed dependencies not be enabled.
// 3. Continue using a new runspace for invoking Get-FunctionsMetadata, but initialize it in worker init and
// point the PsModulePath to the module path bundled with the worker.


InitialSessionState initial = InitialSessionState.CreateDefault();
Runspace runspace = RunspaceFactory.CreateRunspace(initial);
runspace.Open();
System.Management.Automation.PowerShell _powershell = System.Management.Automation.PowerShell.Create();
_powershell.Runspace = runspace;

_powershell.AddCommand(GetFunctionsMetadataCmdletName).AddArgument(baseDir);
string outputString = string.Empty;
foreach (PSObject rawMetadata in _powershell.Invoke())
{
if (outputString != string.Empty)
{
throw new Exception(PowerShellWorkerStrings.GetFunctionsMetadataMultipleResultsError);
}
outputString = rawMetadata.ToString();
}
_powershell.Commands.Clear();

List<FunctionInformation> functionInformations = JsonConvert.DeserializeObject<List<FunctionInformation>>(outputString);

foreach(FunctionInformation fi in functionInformations)
{
indexedFunctions.Add(fi.ConvertToRpc());
}

return indexedFunctions;
}
}
}
6 changes: 6 additions & 0 deletions src/resources/PowerShellWorkerStrings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -352,4 +352,10 @@
<data name="DependencySnapshotDoesNotContainAcceptableModuleVersions" xml:space="preserve">
<value>Dependency snapshot '{0}' does not contain acceptable module versions.</value>
</data>
<data name="GetFunctionsMetadataMultipleResultsError" xml:space="preserve">
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you please ping the Team channel to get feedback on the new string resources you added?

<value>Multiple results from metadata cmdlet.</value>
</data>
<data name="InvalidBindingInfoDirection" xml:space="preserve">
<value>Invalid binding direction. Binding name: {0}</value>
</data>
</root>
15 changes: 8 additions & 7 deletions src/worker.config.json
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
{
"description":{
"language":"powershell",
"extensions":[".ps1", ".psm1"],
"defaultExecutablePath":"dotnet",
"defaultWorkerPath":"%FUNCTIONS_WORKER_RUNTIME_VERSION%/Microsoft.Azure.Functions.PowerShellWorker.dll",
"supportedRuntimeVersions":["7", "7.2"],
"description": {
Francisco-Gamino marked this conversation as resolved.
Show resolved Hide resolved
"language": "powershell",
"extensions": [ ".ps1", ".psm1" ],
"defaultExecutablePath": "dotnet",
"defaultWorkerPath": "%FUNCTIONS_WORKER_RUNTIME_VERSION%/Microsoft.Azure.Functions.PowerShellWorker.dll",
"supportedRuntimeVersions": [ "7", "7.2" ],
"defaultRuntimeVersion": "7",
"sanitizeRuntimeVersionRegex":"\\d+\\.?\\d*"
"sanitizeRuntimeVersionRegex": "\\d+\\.?\\d*",
"workerIndexing": "true"
}
}
Loading