Skip to content

Commit fd1bae8

Browse files
surgupta-msftfabiocav
authored andcommitted
Functions host to take a customer specified port in Custom Handler scenario (#11408)
1 parent 67865b3 commit fd1bae8

File tree

13 files changed

+347
-20
lines changed

13 files changed

+347
-20
lines changed

release_notes.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,6 @@
66
-->
77
- Update Python Worker Version to [4.40.2](https://github.com/Azure/azure-functions-python-worker/releases/tag/4.40.2)
88
- Add JitTrace Files for v4.1044
9+
- Remove duplicate function names from sync triggers payload(#11371)
10+
- Avoid emitting empty tag values for health check metrics (#11393)
11+
- Functions host to take a customer specified port in Custom Handler scenario (#11408)

sample/CustomHandler/host.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
"description": {
66
"defaultExecutablePath": "node",
77
"arguments": [ "server.js" ]
8-
}
8+
},
9+
"port": "3456"
910
}
1011
}

src/WebJobs.Script.WebHost/WebJobsScriptHostService.cs

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -630,7 +630,7 @@ public async Task RestartHostAsync(string reason, CancellationToken cancellation
630630
// If we are running in development mode with core tools, do not overlap the restarts.
631631
// Overlapping restarts are problematic when language worker processes are listening
632632
// to the same debug port
633-
if (ShouldEnforceSequentialRestart())
633+
if (ShouldEnforceSequentialRestart(previousHost))
634634
{
635635
stopTask = Orphan(previousHost, cancellationToken);
636636
await stopTask;
@@ -683,16 +683,11 @@ private static void NotifyHostStopping(IHost previousHost)
683683
dispatcher?.PreShutdown();
684684
}
685685

686-
internal bool ShouldEnforceSequentialRestart()
686+
internal bool ShouldEnforceSequentialRestart(IHost host = null)
687687
{
688-
var sequentialRestartSetting = _config.GetSection(ConfigurationSectionNames.SequentialJobHostRestart);
689-
if (sequentialRestartSetting != null)
690-
{
691-
bool.TryParse(sequentialRestartSetting.Value, out bool enforceSequentialOrder);
692-
return enforceSequentialOrder;
693-
}
694-
695-
return false;
688+
var options = host?.Services?.GetService<IOptions<ScriptHostRecycleOptions>>().Value;
689+
options ??= ScriptHostRecycleOptions.Create(_config);
690+
return options.SequentialHostRestartRequired;
696691
}
697692

698693
private void OnHostInitializing(object sender, EventArgs e)
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the MIT License. See License.txt in the project root for license information.
3+
4+
using System.Text.Json;
5+
using System.Text.Json.Serialization;
6+
using Microsoft.Azure.WebJobs.Hosting;
7+
using Microsoft.Azure.WebJobs.Script.Configuration;
8+
using Microsoft.Extensions.Configuration;
9+
10+
namespace Microsoft.Azure.WebJobs.Script
11+
{
12+
/// <summary>
13+
/// Options that control Script Host recycling behavior.
14+
/// </summary>
15+
public sealed class ScriptHostRecycleOptions : IOptionsFormatter
16+
{
17+
/// <summary>
18+
/// Gets or sets a value indicating whether sequential host restarts are required.
19+
/// </summary>
20+
public bool SequentialHostRestartRequired { get; set; }
21+
22+
public static ScriptHostRecycleOptions Create(IConfiguration configuration)
23+
{
24+
ScriptHostRecycleOptions options = new();
25+
options.Configure(configuration);
26+
return options;
27+
}
28+
29+
public string Format()
30+
{
31+
return JsonSerializer.Serialize(this, typeof(ScriptHostRecycleOptions), ScriptHostRecycleOptionsJsonContext.Default);
32+
}
33+
34+
internal void Configure(IConfiguration configuration)
35+
{
36+
var sequentialRestartSetting = configuration.GetSection(ConfigurationSectionNames.SequentialJobHostRestart);
37+
if (sequentialRestartSetting != null)
38+
{
39+
_ = bool.TryParse(sequentialRestartSetting.Value, out bool enforceSequentialOrder);
40+
SequentialHostRestartRequired = enforceSequentialOrder;
41+
}
42+
}
43+
}
44+
45+
[JsonSourceGenerationOptions(GenerationMode = JsonSourceGenerationMode.Serialization, WriteIndented = true)]
46+
[JsonSerializable(typeof(ScriptHostRecycleOptions))]
47+
internal partial class ScriptHostRecycleOptionsJsonContext : JsonSerializerContext;
48+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the MIT License. See License.txt in the project root for license information.
3+
4+
using System;
5+
using Microsoft.Azure.WebJobs.Script.Workers.Http;
6+
using Microsoft.Extensions.Configuration;
7+
using Microsoft.Extensions.Options;
8+
9+
namespace Microsoft.Azure.WebJobs.Script.Configuration
10+
{
11+
internal sealed class ScriptHostRecycleOptionsSetup : IConfigureOptions<ScriptHostRecycleOptions>
12+
{
13+
private readonly IOptions<HttpWorkerOptions> _httpWorkerOptions;
14+
private readonly IConfiguration _configuration;
15+
16+
public ScriptHostRecycleOptionsSetup(
17+
IOptions<HttpWorkerOptions> httpWorkerOptions, IConfiguration configuration)
18+
{
19+
_httpWorkerOptions = httpWorkerOptions ?? throw new ArgumentNullException(nameof(httpWorkerOptions));
20+
_configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
21+
}
22+
23+
public void Configure(ScriptHostRecycleOptions options)
24+
{
25+
options.Configure(_configuration);
26+
27+
// Enforcing sequential host restarts when a user-specified custom handler port is configured to prevent multiple processes from attempting to bind to the same port concurrently.
28+
if (_httpWorkerOptions.Value.IsPortManuallySet)
29+
{
30+
options.SequentialHostRestartRequired = true;
31+
}
32+
}
33+
}
34+
}

src/WebJobs.Script/ScriptHostBuilderExtensions.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -339,6 +339,7 @@ public static IHostBuilder AddScriptHostCore(this IHostBuilder builder, ScriptAp
339339
services.ConfigureOptions<JobHostFunctionTimeoutOptionsSetup>();
340340
services.AddOptions<WorkerConcurrencyOptions>();
341341
services.ConfigureOptions<HttpWorkerOptionsSetup>();
342+
services.ConfigureOptions<ScriptHostRecycleOptionsSetup>();
342343
services.ConfigureOptions<ManagedDependencyOptionsSetup>();
343344
services.AddOptions<FunctionResultAggregatorOptions>()
344345
.Configure<IConfiguration>((o, c) =>

src/WebJobs.Script/WorkerUtilities.cs

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright (c) .NET Foundation. All rights reserved.
1+
// Copyright (c) .NET Foundation. All rights reserved.
22
// Licensed under the MIT License. See License.txt in the project root for license information.
33

44
using System.Net;
@@ -17,5 +17,37 @@ public static int GetUnusedTcpPort()
1717
return port;
1818
}
1919
}
20+
21+
/// <summary>
22+
/// Determines whether the specified port is available.
23+
/// </summary>
24+
internal static bool CanBindToPort(int port)
25+
{
26+
// Try to bind to the port using IPv6 dual mode socket to cover both IPv4 and IPv6.
27+
using var tcpSocket = new Socket(AddressFamily.InterNetworkV6, SocketType.Stream, ProtocolType.Tcp) { DualMode = true };
28+
try
29+
{
30+
tcpSocket.Bind(new IPEndPoint(IPAddress.IPv6Any, port));
31+
return true;
32+
}
33+
catch (SocketException se) when (se.SocketErrorCode == SocketError.AddressAlreadyInUse)
34+
{
35+
return false;
36+
}
37+
catch
38+
{
39+
// Fall back to IPv4 only socket if IPv6 is not supported on the platform.
40+
using var tcpSocketAny = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
41+
try
42+
{
43+
tcpSocketAny.Bind(new IPEndPoint(IPAddress.Any, port));
44+
return true;
45+
}
46+
catch
47+
{
48+
return false;
49+
}
50+
}
51+
}
2052
}
2153
}

src/WebJobs.Script/Workers/Http/Configuration/HttpWorkerOptions.cs

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,44 @@ namespace Microsoft.Azure.WebJobs.Script.Workers.Http
1010
{
1111
public class HttpWorkerOptions : IOptionsFormatter
1212
{
13+
private int? _port;
14+
1315
public CustomHandlerType Type { get; set; }
1416

1517
public HttpWorkerDescription Description { get; set; }
1618

1719
public WorkerProcessArguments Arguments { get; set; }
1820

19-
public int Port { get; set; }
21+
public int Port
22+
{
23+
get
24+
{
25+
if (_port is null)
26+
{
27+
_port = WorkerUtilities.GetUnusedTcpPort(); // Will always be realized during Options setup.
28+
IsPortManuallySet = false;
29+
}
30+
31+
return _port.Value;
32+
}
33+
34+
set
35+
{
36+
// During dynamic allocation of port, the get method will be called before set method and _port will be assigned dynamically.
37+
// Adding a check here to make sure we don't override IsPortManuallySet flag in that case.
38+
if (_port != value)
39+
{
40+
IsPortManuallySet = true;
41+
_port = value;
42+
}
43+
}
44+
}
45+
46+
/// <summary>
47+
/// Gets a value indicating whether the <see cref="Port"/> property value is taken from configuration.
48+
/// True value indicates that the host will use the configured port value rather than allocating a dynamic port.
49+
/// </summary>
50+
public bool IsPortManuallySet { get; private set; }
2051

2152
/// <summary>
2253
/// Gets or sets a value indicating whether the host will forward the request to the worker process.

src/WebJobs.Script/Workers/Http/Configuration/HttpWorkerOptionsSetup.cs

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515

1616
namespace Microsoft.Azure.WebJobs.Script.Workers.Http
1717
{
18-
internal class HttpWorkerOptionsSetup : IConfigureOptions<HttpWorkerOptions>
18+
internal class HttpWorkerOptionsSetup : IConfigureOptions<HttpWorkerOptions>, IValidateOptions<HttpWorkerOptions>
1919
{
2020
private readonly IEnvironment _environment;
2121
private IConfiguration _configuration;
@@ -130,7 +130,6 @@ private void ConfigureWorkerDescription(HttpWorkerOptions options, IConfiguratio
130130

131131
options.Arguments.ExecutableArguments.AddRange(options.Description.Arguments);
132132
options.Arguments.WorkerArguments.AddRange(options.Description.WorkerArguments);
133-
options.Port = WorkerUtilities.GetUnusedTcpPort();
134133
}
135134

136135
private static List<string> GetArgumentList(IConfigurationSection workerConfigSection, string argumentSectionName)
@@ -148,5 +147,22 @@ private static List<string> GetArgumentList(IConfigurationSection workerConfigSe
148147
}
149148
return null;
150149
}
150+
151+
public ValidateOptionsResult Validate(string name, HttpWorkerOptions options)
152+
{
153+
if (options.IsPortManuallySet)
154+
{
155+
var port = options.Port;
156+
157+
if (port == 0 || !WorkerUtilities.CanBindToPort(port))
158+
{
159+
throw new HostConfigurationException($"Unable to bind to port {port} specified in configuration. Please specify a different port or remove the section to allow dynamic binding of port.");
160+
}
161+
162+
_logger.LogInformation("Using port {port} specified via configuration for custom handler.", port);
163+
}
164+
165+
return ValidateOptionsResult.Success;
166+
}
151167
}
152168
}

test/WebJobs.Script.Tests.Integration/WebHostEndToEnd/SamplesEndToEndTests_CustomHandler.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright (c) .NET Foundation. All rights reserved.
1+
// Copyright (c) .NET Foundation. All rights reserved.
22
// Licensed under the MIT License. See License.txt in the project root for license information.
33

44
using System;
@@ -9,6 +9,8 @@
99
using System.Threading.Tasks;
1010
using Microsoft.Azure.WebJobs.Script.Config;
1111
using Microsoft.Azure.WebJobs.Script.Workers.Rpc;
12+
using Microsoft.Extensions.DependencyInjection;
13+
using Microsoft.Extensions.Options;
1214
using Microsoft.WebJobs.Script.Tests;
1315
using Newtonsoft.Json.Linq;
1416
using Xunit;
@@ -50,6 +52,9 @@ private async Task InvokeHttpTrigger(string functionName)
5052
var response = await _fixture.Host.HttpClient.SendAsync(request);
5153
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
5254

55+
var options = _fixture.Host?.JobHostServices?.GetService<IOptions<ScriptHostRecycleOptions>>();
56+
Assert.True(options.Value.SequentialHostRestartRequired);
57+
5358
string responseContent = await response.Content.ReadAsStringAsync();
5459
JObject res = JObject.Parse(responseContent);
5560
Assert.True(res["functionName"].ToString().StartsWith($"api/{functionName}"));

0 commit comments

Comments
 (0)