Skip to content

Commit c956de4

Browse files
Update language worker to support parsing command-line arguments prefix with functions-<argumentname> (#997)
* Update language worker to support parsing command-line arguments prefix with functions-<argumentname> (#993) * Populate language worker metadata in init response (#884) * Updated subtree from https://github.com/azure/azure-functions-language-worker-protobuf. Tag: v1.5.8-protofile. Commit: 14b2ba5ccb188c160c0f6c519ec1d4521ee36440 (#876)
1 parent 5c90d7e commit c956de4

File tree

5 files changed

+165
-35
lines changed

5 files changed

+165
-35
lines changed

protobuf/README.md

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -36,15 +36,6 @@ From within the Azure Functions language worker repo:
3636
- `git commit -m "Updated subtree from https://github.com/azure/azure-functions-language-worker-protobuf. Tag: <tag-name>. Commit: <commit hash>"`
3737
- `git push`
3838

39-
## Releasing a Language Worker Protobuf version
40-
41-
1. Draft a release in the GitHub UI
42-
- Be sure to include details of the release
43-
2. Create a release version, following semantic versioning guidelines ([semver.org](https://semver.org/))
44-
3. Tag the version with the pattern: `v<M>.<m>.<p>-protofile` (example: `v1.1.0-protofile`)
45-
4. Merge `dev` to `main`
46-
5. Run the release you'd created
47-
4839
## Consuming FunctionRPC.proto
4940
*Note: Update versionNumber before running following commands*
5041

protobuf/src/proto/FunctionRpc.proto

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@ message StreamingMessage {
3232
WorkerInitRequest worker_init_request = 17;
3333
// Worker responds after initializing with its capabilities & status
3434
WorkerInitResponse worker_init_response = 16;
35+
36+
// Worker periodically sends empty heartbeat message to host
37+
WorkerHeartbeat worker_heartbeat = 15;
3538

3639
// Host sends terminate message to worker.
3740
// Worker terminates if it can, otherwise host terminates after a grace period
@@ -117,13 +120,35 @@ message WorkerInitRequest {
117120

118121
// Worker responds with the result of initializing itself
119122
message WorkerInitResponse {
120-
// Version of worker
123+
// NOT USED
124+
// TODO: Remove from protobuf during next breaking change release
121125
string worker_version = 1;
126+
122127
// A map of worker supported features/capabilities
123128
map<string, string> capabilities = 2;
124129

125130
// Status of the response
126131
StatusResult result = 3;
132+
133+
// Worker metadata captured for telemetry purposes
134+
WorkerMetadata worker_metadata = 4;
135+
}
136+
137+
message WorkerMetadata {
138+
// The runtime/stack name
139+
string runtime_name = 1;
140+
141+
// The version of the runtime/stack
142+
string runtime_version = 2;
143+
144+
// The version of the worker
145+
string worker_version = 3;
146+
147+
// The worker bitness/architecture
148+
string worker_bitness = 4;
149+
150+
// Optional additional custom properties
151+
map<string, string> custom_properties = 5;
127152
}
128153

129154
// Used by the host to determine success/failure/cancellation
@@ -134,6 +159,7 @@ message StatusResult {
134159
Success = 1;
135160
Cancelled = 2;
136161
}
162+
137163
// Status for the given result
138164
Status status = 4;
139165

@@ -147,6 +173,10 @@ message StatusResult {
147173
repeated RpcLog logs = 3;
148174
}
149175

176+
// NOT USED
177+
// TODO: Remove from protobuf during next breaking change release
178+
message WorkerHeartbeat {}
179+
150180
// Warning before killing the process after grace_period
151181
// Worker self terminates ..no response on this
152182
message WorkerTerminate {
@@ -291,6 +321,11 @@ message RpcFunctionMetadata {
291321

292322
// A flag indicating if managed dependency is enabled or not
293323
bool managed_dependency_enabled = 14;
324+
325+
// Properties for function metadata
326+
// They're usually specific to a worker and largely passed along to the controller API for use
327+
// outside the host
328+
map<string,string> Properties = 16;
294329
}
295330

296331
// Host tells worker it is ready to receive metadata
@@ -549,11 +584,11 @@ message RpcException {
549584

550585
// Worker specifies whether exception is a user exception,
551586
// for purpose of application insights logging. Defaults to false.
552-
optional bool is_user_exception = 4;
587+
bool is_user_exception = 4;
553588

554589
// Type of exception. If it's a user exception, the type is passed along to app insights.
555590
// Otherwise, it's ignored for now.
556-
optional string type = 5;
591+
string type = 5;
557592
}
558593

559594
// Http cookie type. Note that only name and value are used for Http requests

src/RequestProcessor.cs

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,15 @@ namespace Microsoft.Azure.Functions.PowerShellWorker
2020
{
2121
using System.Diagnostics;
2222
using LogLevel = Microsoft.Azure.WebJobs.Script.Grpc.Messages.RpcLog.Types.Level;
23+
using System.Runtime.InteropServices;
2324

2425
internal class RequestProcessor
2526
{
2627
private readonly MessagingStream _msgStream;
2728
private readonly System.Management.Automation.PowerShell _firstPwshInstance;
2829
private readonly PowerShellManagerPool _powershellPool;
2930
private DependencyManager _dependencyManager;
31+
private string _pwshVersion;
3032

3133
// Holds the exception if an issue is encountered while processing the function app dependencies.
3234
private Exception _initTerminatingError;
@@ -37,11 +39,12 @@ internal class RequestProcessor
3739
private Dictionary<StreamingMessage.ContentOneofCase, Func<StreamingMessage, StreamingMessage>> _requestHandlers =
3840
new Dictionary<StreamingMessage.ContentOneofCase, Func<StreamingMessage, StreamingMessage>>();
3941

40-
internal RequestProcessor(MessagingStream msgStream, System.Management.Automation.PowerShell firstPwshInstance)
42+
internal RequestProcessor(MessagingStream msgStream, System.Management.Automation.PowerShell firstPwshInstance, string pwshVersion)
4143
{
4244
_msgStream = msgStream;
4345
_firstPwshInstance = firstPwshInstance;
4446
_powershellPool = new PowerShellManagerPool(() => new RpcLogger(msgStream));
47+
_pwshVersion = pwshVersion;
4548

4649
// Host sends capabilities/init data to worker
4750
_requestHandlers.Add(StreamingMessage.ContentOneofCase.WorkerInitRequest, ProcessWorkerInitRequest);
@@ -95,6 +98,9 @@ internal async Task ProcessRequestLoop()
9598

9699
internal StreamingMessage ProcessWorkerInitRequest(StreamingMessage request)
97100
{
101+
var stopwatch = new Stopwatch();
102+
stopwatch.Start();
103+
98104
var workerInitRequest = request.WorkerInitRequest;
99105
Environment.SetEnvironmentVariable("AZUREPS_HOST_ENVIRONMENT", $"AzureFunctions/{workerInitRequest.HostVersion}");
100106
Environment.SetEnvironmentVariable("POWERSHELL_DISTRIBUTION_CHANNEL", $"Azure-Functions:{workerInitRequest.HostVersion}");
@@ -117,6 +123,22 @@ internal StreamingMessage ProcessWorkerInitRequest(StreamingMessage request)
117123
RemoteSessionNamedPipeServer.CreateCustomNamedPipeServer(pipeName);
118124
}
119125

126+
try
127+
{
128+
var rpcLogger = new RpcLogger(_msgStream);
129+
rpcLogger.SetContext(request.RequestId, null);
130+
131+
response.WorkerInitResponse.WorkerMetadata = GetWorkerMetadata(_pwshVersion);
132+
133+
rpcLogger.Log(isUserOnlyLog: false, LogLevel.Trace, string.Format(PowerShellWorkerStrings.WorkerInitCompleted, stopwatch.ElapsedMilliseconds));
134+
}
135+
catch (Exception e)
136+
{
137+
status.Status = StatusResult.Types.Status.Failure;
138+
status.Exception = e.ToRpcException();
139+
return response;
140+
}
141+
120142
return response;
121143
}
122144

@@ -180,11 +202,10 @@ internal StreamingMessage ProcessFunctionLoadRequest(StreamingMessage request)
180202
return response;
181203
}
182204

183-
// Ideally, the initialization should happen when processing 'WorkerInitRequest', however, the 'WorkerInitRequest'
184-
// message doesn't provide information about the FunctionApp. That information is not available until the first
185-
// 'FunctionLoadRequest' comes in. Therefore, we run initialization here.
186-
// Also, we receive a FunctionLoadRequest when a proxy is configured. Proxies don't have the Metadata.Directory set
187-
// which would cause initialization issues with the PSModulePath. Since they don't have that set, we skip over them.
205+
// Ideally, the initialization should happen when processing 'WorkerInitRequest'. However, we defer the initialization
206+
// until the first 'FunctionLoadRequest' which contains the information about whether Managed Dependencies is enabled for the function app,
207+
// and if it is, we add the Managed Dependencies path to the PSModulePath.
208+
// Also, we receive a FunctionLoadRequest when a proxy is configured. This is just a no-op on the worker size, so we skip over them.
188209
if (!_isFunctionAppInitialized && !functionLoadRequest.Metadata.IsProxy)
189210
{
190211
try
@@ -519,6 +540,17 @@ private void SetupAppRootPathAndModulePath(FunctionLoadRequest functionLoadReque
519540
.InvokeAndClearCommands();
520541
}
521542

543+
private WorkerMetadata GetWorkerMetadata(string pwshVersion)
544+
{
545+
var data = new WorkerMetadata();
546+
data.WorkerBitness = RuntimeInformation.OSArchitecture.ToString();
547+
data.WorkerVersion = typeof(Worker).Assembly.GetName().Version.ToString();
548+
data.RuntimeVersion = pwshVersion;
549+
data.RuntimeName = "powershell";
550+
551+
return data;
552+
}
553+
522554
#endregion
523555
}
524556
}

src/Worker.cs

Lines changed: 86 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -31,26 +31,70 @@ public async static Task Main(string[] args)
3131
LogLevel.Information,
3232
string.Format(PowerShellWorkerStrings.PowerShellWorkerVersion, typeof(Worker).Assembly.GetName().Version));
3333

34-
WorkerArguments arguments = null;
35-
Parser.Default.ParseArguments<WorkerArguments>(args)
36-
.WithParsed(ops => arguments = ops)
37-
.WithNotParsed(err => Environment.Exit(1));
34+
var workerOptions = new WorkerOptions();
35+
36+
var parser = new Parser(settings =>
37+
{
38+
settings.EnableDashDash = true;
39+
settings.IgnoreUnknownArguments = true;
40+
});
41+
parser.ParseArguments<WorkerArguments>(args)
42+
.WithParsed(workerArgs =>
43+
{
44+
// TODO: Remove parsing old command-line arguments that are not prefixed with functions-<argumentname>
45+
// for more information, see https://github.com/Azure/azure-functions-powershell-worker/issues/995
46+
workerOptions.WorkerId = workerArgs.FunctionsWorkerId ?? workerArgs.WorkerId;
47+
workerOptions.RequestId = workerArgs.FunctionsRequestId ?? workerArgs.RequestId;
48+
49+
if (!string.IsNullOrWhiteSpace(workerArgs.FunctionsUri))
50+
{
51+
try
52+
{
53+
// TODO: Update WorkerOptions to have a URI property instead of host name and port number
54+
// for more information, see https://github.com/Azure/azure-functions-powershell-worker/issues/994
55+
var uri = new Uri(workerArgs.FunctionsUri);
56+
workerOptions.Host = uri.Host;
57+
workerOptions.Port = uri.Port;
58+
}
59+
catch (UriFormatException formatEx)
60+
{
61+
var message = $"Invalid URI format: {workerArgs.FunctionsUri}. Error message: {formatEx.Message}";
62+
throw new ArgumentException(message, nameof(workerArgs.FunctionsUri));
63+
}
64+
}
65+
else
66+
{
67+
workerOptions.Host = workerArgs.Host;
68+
workerOptions.Port = workerArgs.Port;
69+
}
70+
71+
// Validate workerOptions
72+
ValidateProperty("WorkerId", workerOptions.WorkerId);
73+
ValidateProperty("RequestId", workerOptions.RequestId);
74+
ValidateProperty("Host", workerOptions.Host);
75+
76+
if (workerOptions.Port <= 0)
77+
{
78+
throw new ArgumentException("Port number has not been initialized", nameof(workerOptions.Port));
79+
}
80+
});
3881

3982
// Create the very first Runspace so the debugger has the target to attach to.
4083
// This PowerShell instance is shared by the first PowerShellManager instance created in the pool,
4184
// and the dependency manager (used to download dependent modules if needed).
4285
var firstPowerShellInstance = Utils.NewPwshInstance();
43-
LogPowerShellVersion(firstPowerShellInstance);
86+
var pwshVersion = Utils.GetPowerShellVersion(firstPowerShellInstance);
87+
LogPowerShellVersion(pwshVersion);
4488
WarmUpPowerShell(firstPowerShellInstance);
4589

46-
var msgStream = new MessagingStream(arguments.Host, arguments.Port);
47-
var requestProcessor = new RequestProcessor(msgStream, firstPowerShellInstance);
90+
var msgStream = new MessagingStream(workerOptions.Host, workerOptions.Port);
91+
var requestProcessor = new RequestProcessor(msgStream, firstPowerShellInstance, pwshVersion);
4892

4993
// Send StartStream message
5094
var startedMessage = new StreamingMessage()
5195
{
52-
RequestId = arguments.RequestId,
53-
StartStream = new StartStream() { WorkerId = arguments.WorkerId }
96+
RequestId = workerOptions.RequestId,
97+
StartStream = new StartStream() { WorkerId = workerOptions.WorkerId }
5498
};
5599

56100
msgStream.Write(startedMessage);
@@ -75,28 +119,53 @@ private static void WarmUpPowerShell(System.Management.Automation.PowerShell fir
75119
.InvokeAndClearCommands();
76120
}
77121

78-
private static void LogPowerShellVersion(System.Management.Automation.PowerShell pwsh)
122+
private static void LogPowerShellVersion(string pwshVersion)
79123
{
80-
var message = string.Format(PowerShellWorkerStrings.PowerShellVersion, Utils.GetPowerShellVersion(pwsh));
124+
var message = string.Format(PowerShellWorkerStrings.PowerShellVersion, pwshVersion);
81125
RpcLogger.WriteSystemLog(LogLevel.Information, message);
82126
}
127+
128+
private static void ValidateProperty(string name, string value)
129+
{
130+
if (string.IsNullOrWhiteSpace(value))
131+
{
132+
throw new ArgumentException($"{name} is null or empty", name);
133+
}
134+
}
83135
}
84136

85137
internal class WorkerArguments
86138
{
87-
[Option("host", Required = true, HelpText = "IP Address used to connect to the Host via gRPC.")]
139+
[Option("host", Required = false, HelpText = "IP Address used to connect to the Host via gRPC.")]
88140
public string Host { get; set; }
89141

90-
[Option("port", Required = true, HelpText = "Port used to connect to the Host via gRPC.")]
142+
[Option("port", Required = false, HelpText = "Port used to connect to the Host via gRPC.")]
91143
public int Port { get; set; }
92144

93-
[Option("workerId", Required = true, HelpText = "Worker ID assigned to this language worker.")]
145+
[Option("workerId", Required = false, HelpText = "Worker ID assigned to this language worker.")]
94146
public string WorkerId { get; set; }
95147

96-
[Option("requestId", Required = true, HelpText = "Request ID used for gRPC communication with the Host.")]
148+
[Option("requestId", Required = false, HelpText = "Request ID used for gRPC communication with the Host.")]
97149
public string RequestId { get; set; }
98150

99-
[Option("grpcMaxMessageLength", Required = false, HelpText = "[Deprecated and ignored] gRPC Maximum message size.")]
100-
public int MaxMessageLength { get; set; }
151+
[Option("functions-uri", Required = false, HelpText = "URI with IP Address and Port used to connect to the Host via gRPC.")]
152+
public string FunctionsUri { get; set; }
153+
154+
[Option("functions-workerid", Required = false, HelpText = "Worker ID assigned to this language worker.")]
155+
public string FunctionsWorkerId { get; set; }
156+
157+
[Option("functions-requestid", Required = false, HelpText = "Request ID used for gRPC communication with the Host.")]
158+
public string FunctionsRequestId { get; set; }
159+
}
160+
161+
internal class WorkerOptions
162+
{
163+
public string Host { get; set; }
164+
165+
public int Port { get; set; }
166+
167+
public string WorkerId { get; set; }
168+
169+
public string RequestId { get; set; }
101170
}
102171
}

src/resources/PowerShellWorkerStrings.resx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -355,4 +355,7 @@
355355
<data name="AutomaticUpgradesAreDisabled" xml:space="preserve">
356356
<value>Automatic upgrades are disabled in PowerShell 7.0 function apps. To enable this functionality back, please migrate your function app to PowerShell 7.2. For more details, see https://aka.ms/functions-powershell-7.0-to-7.2.</value>
357357
</data>
358+
<data name="WorkerInitCompleted" xml:space="preserve">
359+
<value>Worker init request completed in {0} ms.</value>
360+
</data>
358361
</root>

0 commit comments

Comments
 (0)