Skip to content

Commit 52e5c4b

Browse files
SatishRanjandaxian-dbw
authored andcommitted
Managed dependency download on background thread (Azure#193)
1 parent abbc0b3 commit 52e5c4b

File tree

5 files changed

+146
-91
lines changed

5 files changed

+146
-91
lines changed

src/DependencyManagement/DependencyManager.cs

Lines changed: 84 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,14 @@
1414
using Microsoft.Azure.Functions.PowerShellWorker.PowerShell;
1515
using Microsoft.Azure.Functions.PowerShellWorker.Utility;
1616
using LogLevel = Microsoft.Azure.WebJobs.Script.Grpc.Messages.RpcLog.Types.Level;
17+
using Microsoft.Azure.Functions.PowerShellWorker.Messaging;
1718

1819
namespace Microsoft.Azure.Functions.PowerShellWorker.DependencyManagement
1920
{
2021
using System.Management.Automation;
2122
using System.Management.Automation.Language;
23+
using System.Management.Automation.Runspaces;
24+
using System.Threading.Tasks;
2225

2326
internal class DependencyManager
2427
{
@@ -28,6 +31,11 @@ internal class DependencyManager
2831
// This is the location where the dependent modules will be installed.
2932
internal static string DependenciesPath { get; private set; }
3033

34+
internal Exception DependencyError => _dependencyError;
35+
36+
//The dependency download task
37+
internal Task DependencyDownloadTask => _dependencyDownloadTask;
38+
3139
// Az module name.
3240
private const string AzModuleName = "Az";
3341

@@ -51,6 +59,12 @@ internal class DependencyManager
5159
// Managed Dependencies folder name.
5260
private const string ManagedDependenciesFolderName = "ManagedDependencies";
5361

62+
//Set when any error occurs while downloading dependencies
63+
private Exception _dependencyError;
64+
65+
//Dependency download task
66+
private Task _dependencyDownloadTask;
67+
5468
// This flag is used to figure out if we need to install/reinstall all the function app dependencies.
5569
// If we do, we use it to clean up the module destination path.
5670
private bool _shouldUpdateFunctionAppDependencies;
@@ -60,6 +74,74 @@ internal DependencyManager()
6074
Dependencies = new List<DependencyInfo>();
6175
}
6276

77+
/// <summary>
78+
/// Processes the dependency download request
79+
/// </summary>
80+
/// <param name="msgStream">The protobuf messaging stream</param>
81+
/// <param name="request">The StreamingMessage request for function load</param>
82+
internal void ProcessDependencyDownload(MessagingStream msgStream, StreamingMessage request)
83+
{
84+
if (request.FunctionLoadRequest.ManagedDependencyEnabled)
85+
{
86+
var rpcLogger = new RpcLogger(msgStream);
87+
rpcLogger.SetContext(request.RequestId, null);
88+
if (Dependencies.Count == 0)
89+
{
90+
// If there are no dependencies to install, log and return.
91+
rpcLogger.Log(LogLevel.Trace, PowerShellWorkerStrings.FunctionAppDoesNotHaveDependentModulesToInstall, isUserLog: true);
92+
return;
93+
}
94+
95+
if (!_shouldUpdateFunctionAppDependencies)
96+
{
97+
// The function app already has the latest dependencies installed.
98+
rpcLogger.Log(LogLevel.Trace, PowerShellWorkerStrings.LatestFunctionAppDependenciesAlreadyInstalled, isUserLog: true);
99+
return;
100+
}
101+
102+
//Start dependency download on a separate thread
103+
_dependencyDownloadTask = Task.Run(() => ProcessDependencies(rpcLogger));
104+
}
105+
}
106+
107+
/// <summary>
108+
/// Waits for the dependency download task to finish
109+
/// and sets it's reference to null to be picked for cleanup by next run of GC
110+
/// </summary>
111+
internal void WaitOnDependencyDownload()
112+
{
113+
if (_dependencyDownloadTask != null)
114+
{
115+
_dependencyDownloadTask.Wait();
116+
_dependencyDownloadTask = null;
117+
}
118+
}
119+
120+
private void ProcessDependencies(RpcLogger rpcLogger)
121+
{
122+
try
123+
{
124+
_dependencyError = null;
125+
var initialSessionState = InitialSessionState.CreateDefault();
126+
initialSessionState.ThreadOptions = PSThreadOptions.UseCurrentThread;
127+
initialSessionState.EnvironmentVariables.Add(new SessionStateVariableEntry("PSModulePath", FunctionLoader.FunctionModulePath, null));
128+
// Setting the execution policy on macOS and Linux throws an exception so only update it on Windows
129+
if (Platform.IsWindows)
130+
{
131+
initialSessionState.ExecutionPolicy = Microsoft.PowerShell.ExecutionPolicy.Unrestricted;
132+
}
133+
134+
using (PowerShell powerShellInstance = PowerShell.Create(initialSessionState))
135+
{
136+
InstallFunctionAppDependencies(powerShellInstance, rpcLogger);
137+
}
138+
}
139+
catch (Exception e)
140+
{
141+
_dependencyError = e;
142+
}
143+
}
144+
63145
/// <summary>
64146
/// Initializes the dependency manger and performs the following:
65147
/// - Parse functionAppRoot\requirements.psd1 file and create a list of dependencies to install.
@@ -83,8 +165,8 @@ internal void Initialize(FunctionLoadRequest request)
83165
foreach (DictionaryEntry entry in entries)
84166
{
85167
// A valid entry is of the form: 'ModuleName'='MajorVersion.*"
86-
string name = (string) entry.Key;
87-
string version = (string) entry.Value;
168+
string name = (string)entry.Key;
169+
string version = (string)entry.Value;
88170

89171
// Validates that the module name is a supported dependency.
90172
ValidateModuleName(name);
@@ -124,20 +206,6 @@ internal void InstallFunctionAppDependencies(PowerShell pwsh, ILogger logger)
124206
{
125207
try
126208
{
127-
if (Dependencies.Count == 0)
128-
{
129-
// If there are no dependencies to install, log and return.
130-
logger.Log(LogLevel.Trace, PowerShellWorkerStrings.FunctionAppDoesNotHaveDependentModulesToInstall, isUserLog: true);
131-
return;
132-
}
133-
134-
if (!_shouldUpdateFunctionAppDependencies)
135-
{
136-
// The function app already has the latest dependencies installed.
137-
logger.Log(LogLevel.Trace, PowerShellWorkerStrings.LatestFunctionAppDependenciesAlreadyInstalled, isUserLog: true);
138-
return;
139-
}
140-
141209
// Install the function dependencies.
142210
logger.Log(LogLevel.Trace, PowerShellWorkerStrings.InstallingFunctionAppDependentModules, isUserLog: true);
143211

src/PowerShell/PowerShellManager.cs

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ static PowerShellManager()
4545
addMethod.Invoke(null, new object[] { "HttpRequestContext", typeof(HttpRequestContext) });
4646
}
4747

48-
internal PowerShellManager(ILogger logger, Action<PowerShell, ILogger> initAction = null)
48+
internal PowerShellManager(ILogger logger)
4949
{
5050
if (FunctionLoader.FunctionAppRootPath == null)
5151
{
@@ -77,9 +77,6 @@ internal PowerShellManager(ILogger logger, Action<PowerShell, ILogger> initActio
7777
_pwsh.Streams.Verbose.DataAdding += streamHandler.VerboseDataAdding;
7878
_pwsh.Streams.Warning.DataAdding += streamHandler.WarningDataAdding;
7979

80-
// Install function app dependent modules
81-
initAction?.Invoke(_pwsh, logger);
82-
8380
// Initialize the Runspace
8481
InvokeProfile(FunctionLoader.FunctionAppProfilePath);
8582
}

src/PowerShell/PowerShellManagerPool.cs

Lines changed: 0 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -46,26 +46,6 @@ internal PowerShellManagerPool(MessagingStream msgStream)
4646
RpcLogger.WriteSystemLog(string.Format(PowerShellWorkerStrings.LogConcurrencyUpperBound, _upperBound.ToString()));
4747
}
4848

49-
/// <summary>
50-
/// Initialize the pool and populate it with PowerShellManager instances.
51-
/// We instantiate PowerShellManager instances in a lazy way, starting from size 1 and increase the number of workers as needed.
52-
/// </summary>
53-
internal void Initialize(string requestId, Action<PowerShell, ILogger> initAction = null)
54-
{
55-
var logger = new RpcLogger(_msgStream);
56-
57-
try
58-
{
59-
logger.SetContext(requestId, invocationId: null);
60-
_pool.Add(new PowerShellManager(logger, initAction));
61-
_poolSize = 1;
62-
}
63-
finally
64-
{
65-
logger.ResetContext();
66-
}
67-
}
68-
6949
/// <summary>
7050
/// Checkout an idle PowerShellManager instance in a non-blocking asynchronous way.
7151
/// </summary>

src/RequestProcessor.cs

Lines changed: 28 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
using Microsoft.Azure.WebJobs.Script.Grpc.Messages;
1717
using LogLevel = Microsoft.Azure.WebJobs.Script.Grpc.Messages.RpcLog.Types.Level;
1818

19-
namespace Microsoft.Azure.Functions.PowerShellWorker
19+
namespace Microsoft.Azure.Functions.PowerShellWorker
2020
{
2121
internal class RequestProcessor
2222
{
@@ -57,7 +57,7 @@ internal RequestProcessor(MessagingStream msgStream)
5757

5858
// Host sends required metadata to worker to load function
5959
_requestHandlers.Add(StreamingMessage.ContentOneofCase.FunctionLoadRequest, ProcessFunctionLoadRequest);
60-
60+
6161
// Host requests a given invocation
6262
_requestHandlers.Add(StreamingMessage.ContentOneofCase.InvocationRequest, ProcessInvocationRequest);
6363

@@ -167,7 +167,7 @@ internal StreamingMessage ProcessFunctionLoadRequest(StreamingMessage request)
167167
try
168168
{
169169
_isFunctionAppInitialized = true;
170-
InitializeForFunctionApp(request, response);
170+
InitializeForFunctionApp(request);
171171
}
172172
catch (Exception e)
173173
{
@@ -183,7 +183,6 @@ internal StreamingMessage ProcessFunctionLoadRequest(StreamingMessage request)
183183

184184
try
185185
{
186-
// Load the metadata of the function.
187186
_functionLoader.LoadFunction(functionLoadRequest);
188187
}
189188
catch (Exception e)
@@ -206,6 +205,28 @@ internal StreamingMessage ProcessInvocationRequest(StreamingMessage request)
206205

207206
try
208207
{
208+
if (_dependencyManager.DependencyDownloadTask != null
209+
&& (_dependencyManager.DependencyDownloadTask.Status != TaskStatus.Canceled
210+
|| _dependencyManager.DependencyDownloadTask.Status != TaskStatus.Faulted
211+
|| _dependencyManager.DependencyDownloadTask.Status != TaskStatus.RanToCompletion))
212+
{
213+
var rpcLogger = new RpcLogger(_msgStream);
214+
rpcLogger.SetContext(request.RequestId, request.InvocationRequest?.InvocationId);
215+
rpcLogger.Log(LogLevel.Information, PowerShellWorkerStrings.DependencyDownloadInProgress, isUserLog: true);
216+
_dependencyManager.WaitOnDependencyDownload();
217+
}
218+
219+
if (_dependencyManager.DependencyError != null)
220+
{
221+
StreamingMessage response = NewStreamingMessageTemplate(request.RequestId,
222+
StreamingMessage.ContentOneofCase.InvocationResponse,
223+
out StatusResult status);
224+
status.Status = StatusResult.Types.Status.Failure;
225+
status.Exception = _dependencyManager.DependencyError.ToRpcException();
226+
response.InvocationResponse.InvocationId = request.InvocationRequest.InvocationId;
227+
return response;
228+
}
229+
209230
functionInfo = _functionLoader.GetFunctionInfo(request.InvocationRequest.FunctionId);
210231
psManager = _powershellPool.CheckoutIdleWorker(request, functionInfo);
211232

@@ -296,31 +317,17 @@ internal StreamingMessage ProcessFunctionEnvironmentReloadRequest(StreamingMessa
296317
/// <summary>
297318
/// Initialize the worker based on the FunctionApp that the worker will deal with.
298319
/// </summary>
299-
private void InitializeForFunctionApp(StreamingMessage request, StreamingMessage response)
320+
private void InitializeForFunctionApp(StreamingMessage request)
300321
{
301322
var functionLoadRequest = request.FunctionLoadRequest;
302-
303-
// If 'ManagedDependencyEnabled' is true, process the function app dependencies as defined in FunctionAppRoot\requirements.psd1.
304-
// These dependencies will be installed via 'Save-Module' when the first PowerShellManager instance is being created.
305323
if (functionLoadRequest.ManagedDependencyEnabled)
306324
{
307325
_dependencyManager.Initialize(functionLoadRequest);
308326
}
309327

310328
// Setup the FunctionApp root path and module path.
311329
FunctionLoader.SetupWellKnownPaths(functionLoadRequest);
312-
313-
// Constructing the first PowerShellManager instance for the Pool.
314-
if (DependencyManager.Dependencies.Count > 0)
315-
{
316-
// Do extra work to install the specified dependencies.
317-
_powershellPool.Initialize(request.RequestId, _dependencyManager.InstallFunctionAppDependencies);
318-
response.FunctionLoadResponse.IsDependencyDownloaded = true;
319-
}
320-
else
321-
{
322-
_powershellPool.Initialize(request.RequestId);
323-
}
330+
_dependencyManager.ProcessDependencyDownload(_msgStream, request);
324331
}
325332

326333
/// <summary>
@@ -425,7 +432,7 @@ private void BindOutputFromResult(InvocationResponse response, AzFunctionInfo fu
425432
TypedData dataToUse = transformedValue.ToTypedData();
426433

427434
// if one of the bindings is '$return' we need to set the ReturnValue
428-
if(string.Equals(outBindingName, AzFunctionInfo.DollarReturn, StringComparison.OrdinalIgnoreCase))
435+
if (string.Equals(outBindingName, AzFunctionInfo.DollarReturn, StringComparison.OrdinalIgnoreCase))
429436
{
430437
response.ReturnValue = dataToUse;
431438
continue;

0 commit comments

Comments
 (0)