Skip to content

Commit 3df570c

Browse files
authored
Updating SyncTriggers to include host config (#9898)
1 parent d2157ea commit 3df570c

File tree

12 files changed

+337
-120
lines changed

12 files changed

+337
-120
lines changed

src/WebJobs.Script.WebHost/Controllers/HostController.cs

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,10 @@
1616
using Microsoft.Azure.WebJobs.Host;
1717
using Microsoft.Azure.WebJobs.Host.Executors;
1818
using Microsoft.Azure.WebJobs.Host.Scale;
19+
using Microsoft.Azure.WebJobs.Script.Diagnostics;
1920
using Microsoft.Azure.WebJobs.Script.ExtensionBundle;
2021
using Microsoft.Azure.WebJobs.Script.Scale;
22+
using Microsoft.Azure.WebJobs.Script.WebHost.Extensions;
2123
using Microsoft.Azure.WebJobs.Script.WebHost.Filters;
2224
using Microsoft.Azure.WebJobs.Script.WebHost.Management;
2325
using Microsoft.Azure.WebJobs.Script.WebHost.Models;
@@ -44,6 +46,7 @@ public class HostController : Controller
4446
private readonly IScriptHostManager _scriptHostManager;
4547
private readonly IFunctionsSyncManager _functionsSyncManager;
4648
private readonly HostPerformanceManager _performanceManager;
49+
private readonly IMetricsLogger _metricsLogger;
4750
private static readonly SemaphoreSlim _drainSemaphore = new SemaphoreSlim(1, 1);
4851
private static readonly SemaphoreSlim _resumeSemaphore = new SemaphoreSlim(1, 1);
4952

@@ -52,14 +55,16 @@ public HostController(IOptions<ScriptApplicationHostOptions> applicationHostOpti
5255
IEnvironment environment,
5356
IScriptHostManager scriptHostManager,
5457
IFunctionsSyncManager functionsSyncManager,
55-
HostPerformanceManager performanceManager)
58+
HostPerformanceManager performanceManager,
59+
IMetricsLogger metricsLogger)
5660
{
5761
_applicationHostOptions = applicationHostOptions;
5862
_logger = loggerFactory.CreateLogger(ScriptConstants.LogCategoryHostController);
5963
_environment = environment;
6064
_scriptHostManager = scriptHostManager;
6165
_functionsSyncManager = functionsSyncManager;
6266
_performanceManager = performanceManager;
67+
_metricsLogger = metricsLogger;
6368
}
6469

6570
[HttpGet]
@@ -388,6 +393,22 @@ public IActionResult LaunchDebugger()
388393
[Authorize(Policy = PolicyNames.AdminAuthLevelOrInternal)]
389394
public async Task<IActionResult> SyncTriggers()
390395
{
396+
_metricsLogger.LogEvent(MetricEventNames.SyncTriggersInvoked);
397+
398+
// TEMP: Collecting temporary metrics on the host state during SyncTrigger requests
399+
if (!_scriptHostManager.HostIsInitialized())
400+
{
401+
_metricsLogger.LogEvent(MetricEventNames.SyncTriggersHostNotInitialized);
402+
}
403+
404+
// TODO: We plan on making SyncTriggers RequiresRunningHost across the board,
405+
// but for now only for Flex.
406+
// https://github.com/Azure/azure-functions-host/issues/9904
407+
if (_environment.IsFlexConsumptionSku())
408+
{
409+
await HttpContext.WaitForRunningHostAsync(_scriptHostManager, _applicationHostOptions.Value);
410+
}
411+
391412
var result = await _functionsSyncManager.TrySyncTriggersAsync();
392413

393414
// Return a dummy body to make it valid in ARM template action evaluation

src/WebJobs.Script.WebHost/Extensions/HttpContextExtensions.cs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,41 @@
22
// Licensed under the MIT License. See License.txt in the project root for license information.
33

44
using System.IO;
5+
using System.Net;
56
using System.Threading.Tasks;
67
using Microsoft.AspNetCore.Http;
8+
using Microsoft.AspNetCore.Mvc.Filters;
79
using Microsoft.Azure.WebJobs.Script.Extensions;
810

911
namespace Microsoft.Azure.WebJobs.Script.WebHost.Extensions
1012
{
1113
internal static class HttpContextExtensions
1214
{
15+
public static async Task WaitForRunningHostAsync(this HttpContext httpContext, IScriptHostManager hostManager, ScriptApplicationHostOptions applicationHostOptions, int timeoutSeconds = ScriptConstants.HostTimeoutSeconds, int pollingIntervalMilliseconds = ScriptConstants.HostPollingIntervalMilliseconds, ActionExecutionDelegate next = null)
16+
{
17+
if (hostManager.State == ScriptHostState.Offline)
18+
{
19+
await httpContext.SetOfflineResponseAsync(applicationHostOptions.ScriptPath);
20+
}
21+
else
22+
{
23+
// If the host is not ready, we'll wait a bit for it to initialize.
24+
// This might happen if http requests come in while the host is starting
25+
// up for the first time, or if it is restarting.
26+
bool hostReady = await hostManager.DelayUntilHostReady(timeoutSeconds, pollingIntervalMilliseconds);
27+
28+
if (!hostReady)
29+
{
30+
throw new HttpException(HttpStatusCode.ServiceUnavailable, "Function host is not running.");
31+
}
32+
33+
if (next != null)
34+
{
35+
await next();
36+
}
37+
}
38+
}
39+
1340
public static async Task SetOfflineResponseAsync(this HttpContext httpContext, string scriptPath)
1441
{
1542
httpContext.Response.StatusCode = StatusCodes.Status503ServiceUnavailable;

src/WebJobs.Script/Host/ScriptHostManagerExtensions.cs renamed to src/WebJobs.Script.WebHost/Extensions/ScriptHostManagerExtensions.cs

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

4-
using System;
5-
using System.Collections.Generic;
6-
using System.Text;
74
using System.Threading.Tasks;
85

96
namespace Microsoft.Azure.WebJobs.Script
@@ -12,22 +9,27 @@ public static class ScriptHostManagerExtensions
129
{
1310
public static async Task<bool> DelayUntilHostReady(this IScriptHostManager hostManager, int timeoutSeconds = ScriptConstants.HostTimeoutSeconds, int pollingIntervalMilliseconds = ScriptConstants.HostPollingIntervalMilliseconds)
1411
{
15-
if (CanInvoke(hostManager))
12+
if (HostIsInitialized(hostManager))
1613
{
1714
return true;
1815
}
1916
else
2017
{
2118
await Utility.DelayAsync(timeoutSeconds, pollingIntervalMilliseconds, () =>
2219
{
23-
return !CanInvoke(hostManager) && hostManager.State != ScriptHostState.Error;
20+
return !HostIsInitialized(hostManager) && hostManager.State != ScriptHostState.Error;
2421
});
2522

26-
return CanInvoke(hostManager);
23+
return HostIsInitialized(hostManager);
2724
}
2825
}
2926

30-
public static bool CanInvoke(this IScriptHostManager hostManager)
27+
/// <summary>
28+
/// Gets a value indicating whether the host in an initialized or running state.
29+
/// </summary>
30+
/// <param name="hostManager">The <see cref="IScriptHostManager"/> to check.</param>
31+
/// <returns>True if the host is initialized or running, false otherwise.</returns>
32+
public static bool HostIsInitialized(this IScriptHostManager hostManager)
3133
{
3234
return hostManager.State == ScriptHostState.Running || hostManager.State == ScriptHostState.Initialized;
3335
}

src/WebJobs.Script.WebHost/Filters/RequiresRunningHostAttribute.cs

Lines changed: 1 addition & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
// Licensed under the MIT License. See License.txt in the project root for license information.
33

44
using System;
5-
using System.Net;
65
using System.Threading.Tasks;
76
using Microsoft.AspNetCore.Mvc.Filters;
87
using Microsoft.Azure.WebJobs.Script.WebHost.Extensions;
@@ -64,24 +63,7 @@ public RunningHostCheckAttribute(IScriptHostManager hostManager, IOptionsMonitor
6463

6564
public override async Task OnActionExecutionAsync(ActionExecutingContext actionContext, ActionExecutionDelegate next)
6665
{
67-
if (_hostManager.State == ScriptHostState.Offline)
68-
{
69-
await actionContext.HttpContext.SetOfflineResponseAsync(_applicationHostOptions.CurrentValue.ScriptPath);
70-
}
71-
else
72-
{
73-
// If the host is not ready, we'll wait a bit for it to initialize.
74-
// This might happen if http requests come in while the host is starting
75-
// up for the first time, or if it is restarting.
76-
bool hostReady = await _hostManager.DelayUntilHostReady(TimeoutSeconds, PollingIntervalMilliseconds);
77-
78-
if (!hostReady)
79-
{
80-
throw new HttpException(HttpStatusCode.ServiceUnavailable, "Function host is not running.");
81-
}
82-
83-
await next();
84-
}
66+
await actionContext.HttpContext.WaitForRunningHostAsync(_hostManager, _applicationHostOptions.CurrentValue, TimeoutSeconds, PollingIntervalMilliseconds, next);
8567
}
8668
}
8769
}

src/WebJobs.Script.WebHost/Management/FunctionsSyncManager.cs

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
using Azure.Storage.Blobs;
1515
using Microsoft.Azure.WebJobs.Host.Executors;
1616
using Microsoft.Azure.WebJobs.Host.Storage;
17+
using Microsoft.Azure.WebJobs.Logging;
1718
using Microsoft.Azure.WebJobs.Script.Config;
1819
using Microsoft.Azure.WebJobs.Script.Description;
1920
using Microsoft.Azure.WebJobs.Script.Models;
@@ -63,10 +64,11 @@ public class FunctionsSyncManager : IFunctionsSyncManager, IDisposable
6364
private readonly SemaphoreSlim _syncSemaphore = new SemaphoreSlim(1, 1);
6465
private readonly IAzureBlobStorageProvider _azureBlobStorageProvider;
6566
private readonly IOptions<FunctionsHostingConfigOptions> _hostingConfigOptions;
67+
private readonly IScriptHostManager _scriptHostManager;
6668

6769
private BlobClient _hashBlobClient;
6870

69-
public FunctionsSyncManager(IHostIdProvider hostIdProvider, IOptionsMonitor<ScriptApplicationHostOptions> applicationHostOptions, ILogger<FunctionsSyncManager> logger, IHttpClientFactory httpClientFactory, ISecretManagerProvider secretManagerProvider, IScriptWebHostEnvironment webHostEnvironment, IEnvironment environment, HostNameProvider hostNameProvider, IFunctionMetadataManager functionMetadataManager, IAzureBlobStorageProvider azureBlobStorageProvider, IOptions<FunctionsHostingConfigOptions> functionsHostingConfigOptions)
71+
public FunctionsSyncManager(IHostIdProvider hostIdProvider, IOptionsMonitor<ScriptApplicationHostOptions> applicationHostOptions, ILogger<FunctionsSyncManager> logger, IHttpClientFactory httpClientFactory, ISecretManagerProvider secretManagerProvider, IScriptWebHostEnvironment webHostEnvironment, IEnvironment environment, HostNameProvider hostNameProvider, IFunctionMetadataManager functionMetadataManager, IAzureBlobStorageProvider azureBlobStorageProvider, IOptions<FunctionsHostingConfigOptions> functionsHostingConfigOptions, IScriptHostManager scriptHostManager)
7072
{
7173
_applicationHostOptions = applicationHostOptions;
7274
_logger = logger;
@@ -79,6 +81,7 @@ public FunctionsSyncManager(IHostIdProvider hostIdProvider, IOptionsMonitor<Scri
7981
_functionMetadataManager = functionMetadataManager;
8082
_azureBlobStorageProvider = azureBlobStorageProvider;
8183
_hostingConfigOptions = functionsHostingConfigOptions;
84+
_scriptHostManager = scriptHostManager;
8285
}
8386

8487
internal bool ArmCacheEnabled
@@ -313,9 +316,22 @@ public async Task<SyncTriggersPayload> GetSyncTriggersPayload()
313316
var triggersArray = new JArray(triggers);
314317
int count = triggersArray.Count;
315318

319+
JObject hostConfig = null;
320+
if (_environment.IsFlexConsumptionSku())
321+
{
322+
// TODO: Only adding host configuration for Flex. Once SyncTriggers is marked RequiresRunningHost,
323+
// we'll enable across the board.
324+
// https://github.com/Azure/azure-functions-host/issues/9904
325+
// If an active host is running, add host configuration.
326+
if (Utility.TryGetHostService<IHostOptionsProvider>(_scriptHostManager, out IHostOptionsProvider hostOptionsProvider))
327+
{
328+
hostConfig = hostOptionsProvider.GetOptions();
329+
}
330+
}
331+
316332
// Form the base minimal result
317333
string hostId = await _hostIdProvider.GetHostIdAsync(CancellationToken.None);
318-
JObject result = GetMinimalPayload(hostId, triggersArray);
334+
JObject result = GetMinimalPayload(hostId, triggersArray, hostConfig);
319335

320336
if (!ArmCacheEnabled)
321337
{
@@ -396,7 +412,7 @@ public async Task<SyncTriggersPayload> GetSyncTriggersPayload()
396412
// If we're over limit, revert to the minimal triggers format.
397413
_logger.LogWarning($"SyncTriggers payload of length '{json.Length}' exceeds max length of '{ScriptConstants.MaxTriggersStringLength}'. Reverting to minimal format.");
398414

399-
var minimalResult = GetMinimalPayload(hostId, triggersArray);
415+
var minimalResult = GetMinimalPayload(hostId, triggersArray, hostConfig);
400416
json = JsonConvert.SerializeObject(minimalResult);
401417
}
402418

@@ -407,14 +423,21 @@ public async Task<SyncTriggersPayload> GetSyncTriggersPayload()
407423
};
408424
}
409425

410-
private JObject GetMinimalPayload(string hostId, JArray triggersArray)
426+
private JObject GetMinimalPayload(string hostId, JArray triggersArray, JObject hostConfig)
411427
{
412428
// When the HostId is sent, ScaleController will use it directly rather than compute it itself.
413-
return new JObject
429+
var result = new JObject
414430
{
415431
{ "triggers", triggersArray },
416432
{ "hostId", hostId }
417433
};
434+
435+
if (hostConfig != null)
436+
{
437+
result.Add("hostConfig", Sanitizer.Sanitize(hostConfig));
438+
}
439+
440+
return result;
418441
}
419442

420443
internal static async Task<JObject> GetHostJsonExtensionsAsync(IOptionsMonitor<ScriptApplicationHostOptions> applicationHostOptions, ILogger logger)

src/WebJobs.Script.WebHost/Middleware/HostAvailabilityCheckMiddleware.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ public Task Invoke(HttpContext httpContext)
2929
{
3030
if (_scriptHostManager.State != ScriptHostState.Offline)
3131
{
32-
if (!_scriptHostManager.CanInvoke())
32+
if (!_scriptHostManager.HostIsInitialized())
3333
{
3434
// If we're not ready, take the slower/more expensive route and await being ready
3535
return InvokeAwaitingHost(httpContext, _next, _logger, _scriptHostManager);

src/WebJobs.Script/Config/HostJsonFileConfigurationSource.cs

Lines changed: 4 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,6 @@ public class HostJsonFileConfigurationProvider : ConfigurationProvider
5454
"customHandler", "httpWorker", "extensions", "concurrency", ConfigurationSectionNames.SendCanceledInvocationsToWorker
5555
};
5656

57-
private static readonly string[] CredentialNameFragments = new[] { "password", "pwd", "key", "secret", "token", "sas" };
58-
5957
private readonly HostJsonFileConfigurationSource _configurationSource;
6058
private readonly Stack<string> _path;
6159
private readonly ILogger _logger;
@@ -147,7 +145,10 @@ private JObject LoadHostConfigurationFile()
147145
string hostFilePath = Path.Combine(options.ScriptPath, ScriptConstants.HostMetadataFileName);
148146
JObject hostConfigObject = LoadHostConfig(hostFilePath);
149147
hostConfigObject = InitializeHostConfig(hostFilePath, hostConfigObject);
150-
string sanitizedJson = SanitizeHostJson(hostConfigObject);
148+
149+
Func<string, bool> selector = (name) => WellKnownHostJsonProperties.Contains(name);
150+
var sanitizedHostConfigObject = Sanitizer.Sanitize(hostConfigObject, selector);
151+
string sanitizedJson = sanitizedHostConfigObject.ToString();
151152

152153
_logger.HostConfigApplied();
153154

@@ -283,73 +284,6 @@ private JObject TryAddBundleConfiguration(JObject content, string bundleId, stri
283284
}
284285
return content;
285286
}
286-
287-
internal static string SanitizeHostJson(JObject hostJsonObject)
288-
{
289-
static bool IsPotentialCredential(string name)
290-
{
291-
foreach (string fragment in CredentialNameFragments)
292-
{
293-
if (name.Contains(fragment, StringComparison.OrdinalIgnoreCase))
294-
{
295-
return true;
296-
}
297-
}
298-
299-
return false;
300-
}
301-
302-
static JToken Sanitize(JToken token)
303-
{
304-
if (token is JObject obj)
305-
{
306-
JObject sanitized = new JObject();
307-
foreach (var prop in obj)
308-
{
309-
if (IsPotentialCredential(prop.Key))
310-
{
311-
sanitized[prop.Key] = Sanitizer.SecretReplacement;
312-
}
313-
else
314-
{
315-
sanitized[prop.Key] = Sanitize(prop.Value);
316-
}
317-
}
318-
319-
return sanitized;
320-
}
321-
322-
if (token is JArray arr)
323-
{
324-
JArray sanitized = new JArray();
325-
foreach (var value in arr)
326-
{
327-
sanitized.Add(Sanitize(value));
328-
}
329-
330-
return sanitized;
331-
}
332-
333-
if (token.Type == JTokenType.String)
334-
{
335-
return Sanitizer.Sanitize(token.ToString());
336-
}
337-
338-
return token;
339-
}
340-
341-
JObject sanitizedObject = new JObject();
342-
foreach (var propName in WellKnownHostJsonProperties)
343-
{
344-
var propValue = hostJsonObject[propName];
345-
if (propValue != null)
346-
{
347-
sanitizedObject[propName] = Sanitize(propValue);
348-
}
349-
}
350-
351-
return sanitizedObject.ToString();
352-
}
353287
}
354288
}
355289
}

src/WebJobs.Script/Diagnostics/MetricEventNames.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ public static class MetricEventNames
2020
public const string IdentifiableSecretLoaded = "host.secrets.identifiable";
2121
public const string HISStrictModeEnabled = "host.hismode.strict";
2222
public const string HISStrictModeWarn = "host.hismode.warn";
23+
public const string SyncTriggersInvoked = "host.synctriggers.invoke";
24+
public const string SyncTriggersHostNotInitialized = "host.synctriggers.hostnotinitialized";
2325

2426
// Script host level events
2527
public const string ScriptHostManagerBuildScriptHost = "scripthostmanager.buildscripthost.latency";

0 commit comments

Comments
 (0)