Skip to content

Commit 7c6ae82

Browse files
author
Connor McMahon
authored
Update sync trigger API to synch triggers differently for Durable 2.x (#5282)
A temporary fix to make sure that the correct task hub name is used by the scale controller for Durable 2.x when a task hub name is not specified. This fix can be made more simple once we make the scale controller more robust.
1 parent aead204 commit 7c6ae82

File tree

2 files changed

+477
-101
lines changed

2 files changed

+477
-101
lines changed

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

Lines changed: 178 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,12 @@
88
using System.Net.Http;
99
using System.Security.Cryptography;
1010
using System.Text;
11+
using System.Text.RegularExpressions;
1112
using System.Threading;
1213
using System.Threading.Tasks;
1314
using Microsoft.Azure.WebJobs.Host.Executors;
1415
using Microsoft.Azure.WebJobs.Script.Description;
16+
using Microsoft.Azure.WebJobs.Script.Models;
1517
using Microsoft.Azure.WebJobs.Script.WebHost.Extensions;
1618
using Microsoft.Azure.WebJobs.Script.WebHost.Security;
1719
using Microsoft.Extensions.Configuration;
@@ -29,9 +31,18 @@ public class FunctionsSyncManager : IFunctionsSyncManager, IDisposable
2931
private const string HubName = "HubName";
3032
private const string TaskHubName = "taskHubName";
3133
private const string Connection = "connection";
32-
private const string DurableTaskStorageConnectionName = "azureStorageConnectionStringName";
34+
private const string DurableTaskV1StorageConnectionName = "azureStorageConnectionStringName";
35+
private const string DurableTaskV2StorageOptions = "storageOptions";
36+
private const string DurableTaskV2StorageConnectionName = "connectionStringName";
3337
private const string DurableTask = "durableTask";
3438

39+
// 45 alphanumeric characters gives us a buffer in our table/queue/blob container names.
40+
private const int MaxTaskHubNameSize = 45;
41+
private const int MinTaskHubNameSize = 3;
42+
private const string TaskHubPadding = "Hub";
43+
44+
private readonly Regex versionRegex = new Regex(@"Version=(?<majorversion>\d)\.\d\.\d");
45+
3546
private readonly IOptionsMonitor<ScriptApplicationHostOptions> _applicationHostOptions;
3647
private readonly ILogger _logger;
3748
private readonly HttpClient _httpClient;
@@ -323,32 +334,21 @@ public async Task<string> GetSyncTriggersPayload()
323334

324335
internal async Task<IEnumerable<JObject>> GetFunctionTriggers(IEnumerable<FunctionMetadata> functionsMetadata, ScriptJobHostOptions hostOptions)
325336
{
326-
var durableTaskConfig = await ReadDurableTaskConfig();
327337
var triggers = (await functionsMetadata
328338
.Where(f => !f.IsProxy)
329339
.Select(f => f.ToFunctionTrigger(hostOptions))
330340
.WhenAll())
331-
.Where(t => t != null)
332-
.Select(t =>
341+
.Where(t => t != null);
342+
343+
if (triggers.Any(IsDurableTrigger))
344+
{
345+
DurableConfig durableTaskConfig = await ReadDurableTaskConfig();
346+
// If any host level durable config values, we need to apply them to all durable triggers
347+
if (durableTaskConfig.HasValues())
333348
{
334-
// if we have a durableTask hub name and the function trigger is either orchestrationTrigger OR activityTrigger,
335-
// add a property "taskHubName" with durable task hub name.
336-
if (durableTaskConfig.Any()
337-
&& (t["type"]?.ToString().Equals("orchestrationTrigger", StringComparison.OrdinalIgnoreCase) == true
338-
|| t["type"]?.ToString().Equals("activityTrigger", StringComparison.OrdinalIgnoreCase) == true))
339-
{
340-
if (durableTaskConfig.ContainsKey(HubName))
341-
{
342-
t[TaskHubName] = durableTaskConfig[HubName];
343-
}
344-
345-
if (durableTaskConfig.ContainsKey(Connection))
346-
{
347-
t[Connection] = durableTaskConfig[Connection];
348-
}
349-
}
350-
return t;
351-
});
349+
triggers = triggers.Select(t => UpdateDurableFunctionConfig(t, durableTaskConfig));
350+
}
351+
}
352352

353353
if (FileUtility.FileExists(Path.Combine(hostOptions.RootScriptPath, ScriptConstants.ProxyMetadataFileName)))
354354
{
@@ -359,50 +359,169 @@ internal async Task<IEnumerable<JObject>> GetFunctionTriggers(IEnumerable<Functi
359359
return triggers;
360360
}
361361

362-
private async Task<Dictionary<string, string>> ReadDurableTaskConfig()
362+
private static bool IsDurableTrigger(JObject trigger)
363+
{
364+
return trigger["type"]?.ToString().Equals("orchestrationTrigger", StringComparison.OrdinalIgnoreCase) == true
365+
|| trigger["type"]?.ToString().Equals("entityTrigger", StringComparison.OrdinalIgnoreCase) == true
366+
|| trigger["type"]?.ToString().Equals("activityTrigger", StringComparison.OrdinalIgnoreCase) == true;
367+
}
368+
369+
private static JObject UpdateDurableFunctionConfig(JObject trigger, DurableConfig durableTaskConfig)
370+
{
371+
if (IsDurableTrigger(trigger))
372+
{
373+
if (durableTaskConfig.HubName != null)
374+
{
375+
trigger[TaskHubName] = durableTaskConfig.HubName;
376+
}
377+
378+
if (durableTaskConfig.Connection != null)
379+
{
380+
trigger[Connection] = durableTaskConfig.Connection;
381+
}
382+
}
383+
return trigger;
384+
}
385+
386+
private async Task<DurableConfig> ReadDurableTaskConfig()
363387
{
388+
JObject hostJson = null;
389+
JObject durableHostConfig = null;
364390
var hostOptions = _applicationHostOptions.CurrentValue.ToHostOptions();
365391
string hostJsonPath = Path.Combine(hostOptions.RootScriptPath, ScriptConstants.HostMetadataFileName);
366-
var config = new Dictionary<string, string>();
367392
if (FileUtility.FileExists(hostJsonPath))
368393
{
369-
var json = JObject.Parse(await FileUtility.ReadAsync(hostJsonPath));
394+
hostJson = JObject.Parse(await FileUtility.ReadAsync(hostJsonPath));
370395

371396
// get the DurableTask extension config section
372-
JToken extensionsValue;
373-
if (json.TryGetValue("extensions", StringComparison.OrdinalIgnoreCase, out extensionsValue) && extensionsValue != null)
397+
if (hostJson != null &&
398+
hostJson.TryGetValue("extensions", StringComparison.OrdinalIgnoreCase, out JToken extensionsValue))
374399
{
375-
json = (JObject)extensionsValue;
400+
// we will allow case insensitivity given it is likely user hand edited
401+
// see https://github.com/Azure/azure-functions-durable-extension/issues/111
402+
var extensions = extensionsValue as JObject;
403+
if (extensions != null &&
404+
extensions.TryGetValue(DurableTask, StringComparison.OrdinalIgnoreCase, out JToken durableTaskValue))
405+
{
406+
durableHostConfig = durableTaskValue as JObject;
407+
}
376408
}
409+
}
410+
411+
var durableMajorVersion = await GetDurableMajorVersionAsync(hostJson, hostOptions);
412+
if (durableMajorVersion == null || durableMajorVersion.Equals("1"))
413+
{
414+
return GetDurableV1Config(durableHostConfig);
415+
}
416+
else
417+
{
418+
// v2 or greater
419+
return GetDurableV2Config(durableHostConfig);
420+
}
421+
}
422+
423+
// This is a stopgap approach to get the Durable extension version. It duplicates some logic in ExtensionManager.cs.
424+
private async Task<string> GetDurableMajorVersionAsync(JObject hostJson, ScriptJobHostOptions hostOptions)
425+
{
426+
bool isUsingBundles = hostJson != null && hostJson.TryGetValue("extensionBundle", StringComparison.OrdinalIgnoreCase, out _);
427+
if (isUsingBundles)
428+
{
429+
// TODO: As of 2019-12-12, there are no extension bundles for version 2.x of Durable.
430+
// This may change in the future.
431+
return "1";
432+
}
433+
434+
string binPath = binPath = Path.Combine(hostOptions.RootScriptPath, "bin");
435+
string metadataFilePath = Path.Combine(binPath, ScriptConstants.ExtensionsMetadataFileName);
436+
if (!FileUtility.FileExists(metadataFilePath))
437+
{
438+
return null;
439+
}
440+
441+
var extensionMetadata = JObject.Parse(await FileUtility.ReadAsync(metadataFilePath));
442+
var extensionItems = extensionMetadata["extensions"]?.ToObject<List<ExtensionReference>>();
443+
444+
var durableExtension = extensionItems?.FirstOrDefault(ext => string.Equals(ext.Name, "DurableTask", StringComparison.OrdinalIgnoreCase));
445+
if (durableExtension == null)
446+
{
447+
return null;
448+
}
377449

378-
// we will allow case insensitivity given it is likely user hand edited
379-
// see https://github.com/Azure/azure-functions-durable-extension/issues/111
380-
JToken durableTaskValue;
381-
if (json.TryGetValue(DurableTask, StringComparison.OrdinalIgnoreCase, out durableTaskValue) && durableTaskValue != null)
450+
var versionMatch = versionRegex.Match(durableExtension.TypeName);
451+
if (!versionMatch.Success)
452+
{
453+
return null;
454+
}
455+
456+
// Grab the captured group.
457+
return versionMatch.Groups["majorversion"].Captures[0].Value;
458+
}
459+
460+
private DurableConfig GetDurableV1Config(JObject durableHostConfig)
461+
{
462+
var config = new DurableConfig();
463+
if (durableHostConfig != null)
464+
{
465+
if (durableHostConfig.TryGetValue(HubName, StringComparison.OrdinalIgnoreCase, out JToken nameValue) && nameValue != null)
382466
{
383-
try
384-
{
385-
var kvp = (JObject)durableTaskValue;
386-
if (kvp.TryGetValue(HubName, StringComparison.OrdinalIgnoreCase, out JToken nameValue) && nameValue != null)
387-
{
388-
config.Add(HubName, nameValue.ToString());
389-
}
390-
391-
if (kvp.TryGetValue(DurableTaskStorageConnectionName, StringComparison.OrdinalIgnoreCase, out nameValue) && nameValue != null)
392-
{
393-
config.Add(Connection, nameValue.ToString());
394-
}
395-
}
396-
catch (Exception)
467+
config.HubName = nameValue.ToString();
468+
}
469+
470+
if (durableHostConfig.TryGetValue(DurableTaskV1StorageConnectionName, StringComparison.OrdinalIgnoreCase, out nameValue) && nameValue != null)
471+
{
472+
config.Connection = nameValue.ToString();
473+
}
474+
}
475+
476+
return config;
477+
}
478+
479+
private DurableConfig GetDurableV2Config(JObject durableHostConfig)
480+
{
481+
var config = new DurableConfig();
482+
483+
if (durableHostConfig != null)
484+
{
485+
if (durableHostConfig.TryGetValue(HubName, StringComparison.OrdinalIgnoreCase, out JToken nameValue) && nameValue != null)
486+
{
487+
config.HubName = nameValue.ToString();
488+
}
489+
490+
if (durableHostConfig.TryGetValue(DurableTaskV2StorageOptions, StringComparison.OrdinalIgnoreCase, out JToken storageOptions) && (storageOptions as JObject) != null)
491+
{
492+
if (((JObject)storageOptions).TryGetValue(DurableTaskV2StorageConnectionName, StringComparison.OrdinalIgnoreCase, out nameValue) && nameValue != null)
397493
{
398-
throw new InvalidDataException("Invalid host.json configuration for 'durableTask'.");
494+
config.Connection = nameValue.ToString();
399495
}
400496
}
401497
}
402498

499+
if (config.HubName == null)
500+
{
501+
config.HubName = GetDefaultDurableV2HubName();
502+
}
503+
403504
return config;
404505
}
405506

507+
// This logic will eventually be moved to ScaleController once it has access to version information.
508+
private string GetDefaultDurableV2HubName()
509+
{
510+
// See https://github.com/Azure/azure-functions-durable-extension/blob/eb186eadb73a21d0efdc33cd7603fde5d802cab9/src/WebJobs.Extensions.DurableTask/Options/DurableTaskOptions.cs#L42
511+
string hubName = _environment.GetEnvironmentVariable(EnvironmentSettingNames.AzureWebsiteName);
512+
// See https://github.com/Azure/azure-functions-durable-extension/blob/eb186eadb73a21d0efdc33cd7603fde5d802cab9/src/WebJobs.Extensions.DurableTask/Options/AzureStorageOptions.cs#L145
513+
hubName = new string(hubName.ToCharArray()
514+
.Where(char.IsLetterOrDigit)
515+
.Take(MaxTaskHubNameSize)
516+
.ToArray());
517+
if (hubName.Length < MinTaskHubNameSize)
518+
{
519+
hubName += TaskHubPadding;
520+
}
521+
522+
return hubName;
523+
}
524+
406525
internal HttpRequestMessage BuildSetTriggersRequest()
407526
{
408527
var protocol = "https";
@@ -466,5 +585,17 @@ public void Dispose()
466585
{
467586
_syncSemaphore.Dispose();
468587
}
588+
589+
private class DurableConfig
590+
{
591+
public string HubName { get; set; }
592+
593+
public string Connection { get; set; }
594+
595+
public bool HasValues()
596+
{
597+
return this.HubName != null || this.Connection != null;
598+
}
599+
}
469600
}
470601
}

0 commit comments

Comments
 (0)