Skip to content

Commit bf4ba5a

Browse files
authored
Refactor the code base to make it easy to support concurrency within a worker in future (#117)
Refactor the code base to make it easy to support concurrency within a worker in future. - Add `PowerShellManagerPool` , but it's just the skeleton. Today the pool only has one PowerShellManager instance. We can add the real pool implementation if we decide to support concurrency within a worker process. - Setup the `PSModule` environment variable as part of `Runspace.Open` by using `InitialSessionState.EnvironmentVariables`. This fixes the mysterious `ModulePathShouldBeSetCorrectly` test failure by making sure the env variable is set to the expected one as part of the `Runspace.Open` of every `PowerShellManager`. It failed before because: 1. `dotnet test` runs tests in parallel by default 2. when creating a new `PowerShellManager`, the `PSModulePath` environment variable will be set again within `Runspace.Open` (by `SetModulePath` called from `ModuleIntrisic` constructor). That makes value unexpected. - Update tests to make them more reliable.
1 parent 8964b0d commit bf4ba5a

File tree

7 files changed

+223
-168
lines changed

7 files changed

+223
-168
lines changed

src/FunctionLoader.cs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ internal class FunctionLoader
2121

2222
internal static string FunctionAppRootPath { get; private set; }
2323
internal static string FunctionAppProfilePath { get; private set; }
24-
internal static string FunctionAppModulesPath { get; private set; }
24+
internal static string FunctionModulePath { get; private set; }
2525

2626
/// <summary>
2727
/// Query for function metadata can happen in parallel.
@@ -51,9 +51,14 @@ internal void LoadFunction(FunctionLoadRequest request)
5151
/// </summary>
5252
internal static void SetupWellKnownPaths(FunctionLoadRequest request)
5353
{
54+
// Resolve the FunctionApp root path
5455
FunctionAppRootPath = Path.GetFullPath(Path.Join(request.Metadata.Directory, ".."));
55-
FunctionAppModulesPath = Path.Join(FunctionAppRootPath, "Modules");
56+
// Resolve module paths
57+
var appLevelModulesPath = Path.Join(FunctionAppRootPath, "Modules");
58+
var workerLevelModulesPath = Path.Join(AppDomain.CurrentDomain.BaseDirectory, "Modules");
59+
FunctionModulePath = $"{appLevelModulesPath}{Path.PathSeparator}{workerLevelModulesPath}";
5660

61+
// Resolve the FunctionApp profile path
5762
var options = new EnumerationOptions { MatchCasing = MatchCasing.CaseInsensitive };
5863
var profiles = Directory.EnumerateFiles(FunctionAppRootPath, "profile.ps1", options);
5964
FunctionAppProfilePath = profiles.FirstOrDefault();

src/PowerShell/PowerShellManager.cs

Lines changed: 30 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,33 @@ internal class PowerShellManager
2323
private readonly ILogger _logger;
2424
private readonly PowerShell _pwsh;
2525

26+
/// <summary>
27+
/// Gets the Runspace InstanceId.
28+
/// </summary>
29+
internal Guid InstanceId => _pwsh.Runspace.InstanceId;
30+
31+
static PowerShellManager()
32+
{
33+
// Set the type accelerators for 'HttpResponseContext' and 'HttpResponseContext'.
34+
// We probably will expose more public types from the worker in future for the interop between worker and the 'PowerShellWorker' module.
35+
// But it's most likely only 'HttpResponseContext' and 'HttpResponseContext' are supposed to be used directly by users, so we only add
36+
// type accelerators for these two explicitly.
37+
var accelerator = typeof(PSObject).Assembly.GetType("System.Management.Automation.TypeAccelerators");
38+
var addMethod = accelerator.GetMethod("Add", new Type[] { typeof(string), typeof(Type) });
39+
addMethod.Invoke(null, new object[] { "HttpResponseContext", typeof(HttpResponseContext) });
40+
addMethod.Invoke(null, new object[] { "HttpRequestContext", typeof(HttpRequestContext) });
41+
}
42+
2643
internal PowerShellManager(ILogger logger)
2744
{
45+
if (FunctionLoader.FunctionAppRootPath == null)
46+
{
47+
throw new InvalidOperationException($"The FunctionApp root hasn't been resolved yet!");
48+
}
49+
2850
var initialSessionState = InitialSessionState.CreateDefault();
51+
initialSessionState.EnvironmentVariables.Add(
52+
new SessionStateVariableEntry("PSModulePath", FunctionLoader.FunctionModulePath, null));
2953

3054
// Setting the execution policy on macOS and Linux throws an exception so only update it on Windows
3155
if(Platform.IsWindows)
@@ -34,8 +58,9 @@ internal PowerShellManager(ILogger logger)
3458
// Windows client versions. This is needed if a user is testing their function locally with the func CLI
3559
initialSessionState.ExecutionPolicy = Microsoft.PowerShell.ExecutionPolicy.Unrestricted;
3660
}
37-
_pwsh = PowerShell.Create(initialSessionState);
61+
3862
_logger = logger;
63+
_pwsh = PowerShell.Create(initialSessionState);
3964

4065
// Setup Stream event listeners
4166
var streamHandler = new StreamHandler(logger);
@@ -45,34 +70,17 @@ internal PowerShellManager(ILogger logger)
4570
_pwsh.Streams.Progress.DataAdding += streamHandler.ProgressDataAdding;
4671
_pwsh.Streams.Verbose.DataAdding += streamHandler.VerboseDataAdding;
4772
_pwsh.Streams.Warning.DataAdding += streamHandler.WarningDataAdding;
48-
}
4973

50-
/// <summary>
51-
/// This method performs the one-time initialization at the worker process level.
52-
/// </summary>
53-
internal void PerformWorkerLevelInitialization()
54-
{
55-
// Set the type accelerators for 'HttpResponseContext' and 'HttpResponseContext'.
56-
// We probably will expose more public types from the worker in future for the interop between worker and the 'PowerShellWorker' module.
57-
// But it's most likely only 'HttpResponseContext' and 'HttpResponseContext' are supposed to be used directly by users, so we only add
58-
// type accelerators for these two explicitly.
59-
var accelerator = typeof(PSObject).Assembly.GetType("System.Management.Automation.TypeAccelerators");
60-
var addMethod = accelerator.GetMethod("Add", new Type[] { typeof(string), typeof(Type) });
61-
addMethod.Invoke(null, new object[] { "HttpResponseContext", typeof(HttpResponseContext) });
62-
addMethod.Invoke(null, new object[] { "HttpRequestContext", typeof(HttpRequestContext) });
63-
64-
// Set the PSModulePath
65-
var workerModulesPath = Path.Join(AppDomain.CurrentDomain.BaseDirectory, "Modules");
66-
Environment.SetEnvironmentVariable("PSModulePath", $"{FunctionLoader.FunctionAppModulesPath}{Path.PathSeparator}{workerModulesPath}");
74+
// Initialize the Runspace
75+
InvokeProfile(FunctionLoader.FunctionAppProfilePath);
6776
}
6877

6978
/// <summary>
70-
/// This method performs initialization that has to be done for each Runspace, e.g. running the Function App's profile.ps1.
79+
/// This method invokes the FunctionApp's profile.ps1.
7180
/// </summary>
72-
internal void PerformRunspaceLevelInitialization()
81+
internal void InvokeProfile(string profilePath)
7382
{
7483
Exception exception = null;
75-
string profilePath = FunctionLoader.FunctionAppProfilePath;
7684
if (profilePath == null)
7785
{
7886
_logger.Log(LogLevel.Trace, $"No 'profile.ps1' is found at the FunctionApp root folder: {FunctionLoader.FunctionAppRootPath}");
@@ -195,25 +203,6 @@ internal string ConvertToJson(object fromObj)
195203
.InvokeAndClearCommands<string>()[0];
196204
}
197205

198-
/// <summary>
199-
/// Helper method to set the output binding metadata for the function that is about to run.
200-
/// </summary>
201-
internal void RegisterFunctionMetadata(AzFunctionInfo functionInfo)
202-
{
203-
var outputBindings = functionInfo.OutputBindings;
204-
FunctionMetadata.OutputBindingCache.AddOrUpdate(_pwsh.Runspace.InstanceId,
205-
outputBindings,
206-
(key, value) => outputBindings);
207-
}
208-
209-
/// <summary>
210-
/// Helper method to clear the output binding metadata for the function that has done running.
211-
/// </summary>
212-
internal void UnregisterFunctionMetadata()
213-
{
214-
FunctionMetadata.OutputBindingCache.TryRemove(_pwsh.Runspace.InstanceId, out _);
215-
}
216-
217206
private void ResetRunspace(string moduleName)
218207
{
219208
// Reset the runspace to the Initial Session State
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
//
2+
// Copyright (c) Microsoft. All rights reserved.
3+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
4+
//
5+
6+
using System;
7+
using Microsoft.Azure.Functions.PowerShellWorker.Utility;
8+
9+
namespace Microsoft.Azure.Functions.PowerShellWorker.PowerShell
10+
{
11+
using System.Management.Automation;
12+
13+
/// <summary>
14+
/// The PowerShellManager pool for the in-proc concurrency support.
15+
/// </summary>
16+
internal class PowerShellManagerPool
17+
{
18+
private readonly ILogger _logger;
19+
// Today we don't really support the in-proc concurrency. We just hold an instance of PowerShellManager in this field.
20+
private PowerShellManager _psManager;
21+
22+
/// <summary>
23+
/// Constructor of the pool.
24+
/// </summary>
25+
internal PowerShellManagerPool(ILogger logger)
26+
{
27+
_logger = logger;
28+
}
29+
30+
/// <summary>
31+
/// Initialize the pool and populate it with PowerShellManager instances.
32+
/// When it's time to really implement this pool, we probably should instantiate PowerShellManager instances in a lazy way.
33+
/// Maybe start from size 1 and increase the number of workers as needed.
34+
/// </summary>
35+
internal void Initialize()
36+
{
37+
_psManager = new PowerShellManager(_logger);
38+
}
39+
40+
/// <summary>
41+
/// Checkout an idle PowerShellManager instance.
42+
/// When it's time to really implement this pool, this method is supposed to block when there is no idle instance available.
43+
/// </summary>
44+
internal PowerShellManager CheckoutIdleWorker(AzFunctionInfo functionInfo)
45+
{
46+
// Register the function with the Runspace before returning the idle PowerShellManager.
47+
FunctionMetadata.RegisterFunctionMetadata(_psManager.InstanceId, functionInfo);
48+
return _psManager;
49+
}
50+
51+
/// <summary>
52+
/// Return a used PowerShellManager instance to the pool.
53+
/// </summary>
54+
internal void ReclaimUsedWorker(PowerShellManager psManager)
55+
{
56+
if (psManager != null)
57+
{
58+
// Unregister the Runspace before reclaiming the used PowerShellManager.
59+
FunctionMetadata.UnregisterFunctionMetadata(psManager.InstanceId);
60+
}
61+
}
62+
}
63+
}

src/Public/FunctionMetadata.cs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,5 +26,22 @@ public static ReadOnlyDictionary<string, ReadOnlyBindingInfo> GetOutputBindingIn
2626
OutputBindingCache.TryGetValue(runspaceInstanceId, out outputBindings);
2727
return outputBindings;
2828
}
29+
30+
/// <summary>
31+
/// Helper method to set the output binding metadata for the function that is about to run.
32+
/// </summary>
33+
internal static void RegisterFunctionMetadata(Guid instanceId, AzFunctionInfo functionInfo)
34+
{
35+
var outputBindings = functionInfo.OutputBindings;
36+
OutputBindingCache.AddOrUpdate(instanceId, outputBindings, (key, value) => outputBindings);
37+
}
38+
39+
/// <summary>
40+
/// Helper method to clear the output binding metadata for the function that has done running.
41+
/// </summary>
42+
internal static void UnregisterFunctionMetadata(Guid instanceId)
43+
{
44+
OutputBindingCache.TryRemove(instanceId, out _);
45+
}
2946
}
3047
}

src/RequestProcessor.cs

Lines changed: 17 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ internal class RequestProcessor
2020
private readonly FunctionLoader _functionLoader;
2121
private readonly RpcLogger _logger;
2222
private readonly MessagingStream _msgStream;
23-
private readonly PowerShellManager _powerShellManager;
23+
private readonly PowerShellManagerPool _powershellPool;
2424

2525
// Indicate whether the FunctionApp has been initialized.
2626
private bool _isFunctionAppInitialized;
@@ -29,7 +29,7 @@ internal RequestProcessor(MessagingStream msgStream)
2929
{
3030
_msgStream = msgStream;
3131
_logger = new RpcLogger(msgStream);
32-
_powerShellManager = new PowerShellManager(_logger);
32+
_powershellPool = new PowerShellManagerPool(_logger);
3333
_functionLoader = new FunctionLoader();
3434
}
3535

@@ -98,9 +98,7 @@ internal StreamingMessage ProcessFunctionLoadRequest(StreamingMessage request)
9898
if (!_isFunctionAppInitialized)
9999
{
100100
FunctionLoader.SetupWellKnownPaths(functionLoadRequest);
101-
_powerShellManager.PerformWorkerLevelInitialization();
102-
_powerShellManager.PerformRunspaceLevelInitialization();
103-
101+
_powershellPool.Initialize();
104102
_isFunctionAppInitialized = true;
105103
}
106104

@@ -122,6 +120,7 @@ internal StreamingMessage ProcessFunctionLoadRequest(StreamingMessage request)
122120
/// </summary>
123121
internal StreamingMessage ProcessInvocationRequest(StreamingMessage request)
124122
{
123+
PowerShellManager psManager = null;
125124
InvocationRequest invocationRequest = request.InvocationRequest;
126125

127126
StreamingMessage response = NewStreamingMessageTemplate(
@@ -130,18 +129,18 @@ internal StreamingMessage ProcessInvocationRequest(StreamingMessage request)
130129
out StatusResult status);
131130
response.InvocationResponse.InvocationId = invocationRequest.InvocationId;
132131

133-
// Invoke powershell logic and return hashtable of out binding data
134132
try
135133
{
136134
// Load information about the function
137135
var functionInfo = _functionLoader.GetFunctionInfo(invocationRequest.FunctionId);
138-
_powerShellManager.RegisterFunctionMetadata(functionInfo);
136+
psManager = _powershellPool.CheckoutIdleWorker(functionInfo);
139137

138+
// Invoke the function and return a hashtable of out binding data
140139
Hashtable results = functionInfo.Type == AzFunctionType.OrchestrationFunction
141-
? InvokeOrchestrationFunction(functionInfo, invocationRequest)
142-
: InvokeSingleActivityFunction(functionInfo, invocationRequest);
140+
? InvokeOrchestrationFunction(psManager, functionInfo, invocationRequest)
141+
: InvokeSingleActivityFunction(psManager, functionInfo, invocationRequest);
143142

144-
BindOutputFromResult(response.InvocationResponse, functionInfo, results);
143+
BindOutputFromResult(psManager, response.InvocationResponse, functionInfo, results);
145144
}
146145
catch (Exception e)
147146
{
@@ -150,7 +149,7 @@ internal StreamingMessage ProcessInvocationRequest(StreamingMessage request)
150149
}
151150
finally
152151
{
153-
_powerShellManager.UnregisterFunctionMetadata();
152+
_powershellPool.ReclaimUsedWorker(psManager);
154153
}
155154

156155
return response;
@@ -188,15 +187,15 @@ private StreamingMessage NewStreamingMessageTemplate(string requestId, Streaming
188187
/// <summary>
189188
/// Invoke an orchestration function.
190189
/// </summary>
191-
private Hashtable InvokeOrchestrationFunction(AzFunctionInfo functionInfo, InvocationRequest invocationRequest)
190+
private Hashtable InvokeOrchestrationFunction(PowerShellManager psManager, AzFunctionInfo functionInfo, InvocationRequest invocationRequest)
192191
{
193192
throw new NotImplementedException("Durable function is not yet supported for PowerShell");
194193
}
195194

196195
/// <summary>
197196
/// Invoke a regular function or an activity function.
198197
/// </summary>
199-
private Hashtable InvokeSingleActivityFunction(AzFunctionInfo functionInfo, InvocationRequest invocationRequest)
198+
private Hashtable InvokeSingleActivityFunction(PowerShellManager psManager, AzFunctionInfo functionInfo, InvocationRequest invocationRequest)
200199
{
201200
// Bundle all TriggerMetadata into Hashtable to send down to PowerShell
202201
var triggerMetadata = new Hashtable(StringComparer.OrdinalIgnoreCase);
@@ -210,16 +209,13 @@ private Hashtable InvokeSingleActivityFunction(AzFunctionInfo functionInfo, Invo
210209
}
211210
}
212211

213-
return _powerShellManager.InvokeFunction(
214-
functionInfo,
215-
triggerMetadata,
216-
invocationRequest.InputData);
212+
return psManager.InvokeFunction(functionInfo, triggerMetadata, invocationRequest.InputData);
217213
}
218214

219215
/// <summary>
220216
/// Set the 'ReturnValue' and 'OutputData' based on the invocation results appropriately.
221217
/// </summary>
222-
private void BindOutputFromResult(InvocationResponse response, AzFunctionInfo functionInfo, Hashtable results)
218+
private void BindOutputFromResult(PowerShellManager psManager, InvocationResponse response, AzFunctionInfo functionInfo, Hashtable results)
223219
{
224220
switch (functionInfo.Type)
225221
{
@@ -231,14 +227,14 @@ private void BindOutputFromResult(InvocationResponse response, AzFunctionInfo fu
231227
string outBindingName = binding.Key;
232228
if(string.Equals(outBindingName, AzFunctionInfo.DollarReturn, StringComparison.OrdinalIgnoreCase))
233229
{
234-
response.ReturnValue = results[outBindingName].ToTypedData(_powerShellManager);
230+
response.ReturnValue = results[outBindingName].ToTypedData(psManager);
235231
continue;
236232
}
237233

238234
ParameterBinding paramBinding = new ParameterBinding()
239235
{
240236
Name = outBindingName,
241-
Data = results[outBindingName].ToTypedData(_powerShellManager)
237+
Data = results[outBindingName].ToTypedData(psManager)
242238
};
243239

244240
response.OutputData.Add(paramBinding);
@@ -247,7 +243,7 @@ private void BindOutputFromResult(InvocationResponse response, AzFunctionInfo fu
247243

248244
case AzFunctionType.OrchestrationFunction:
249245
case AzFunctionType.ActivityFunction:
250-
response.ReturnValue = results[AzFunctionInfo.DollarReturn].ToTypedData(_powerShellManager);
246+
response.ReturnValue = results[AzFunctionInfo.DollarReturn].ToTypedData(psManager);
251247
break;
252248

253249
default:

0 commit comments

Comments
 (0)