Skip to content

Commit 752d78f

Browse files
author
Connor McMahon
authored
Ensure using latest webhook url for HTTP management APIs (#1716)
Previously, we store the value of the webhook URL during extension initialization with the assumption that this webhook URL never changes after startup. This stored value is used for various IDurableClient methods, along with the out-of-proc clients when local RPC endpoints are disabled. However, it turns out that in the case of slot-swap operations, WEBSITE_HOSTNAME uses the staging slot hostname at site startup, and only after the slot swap does the hostname change. The function host listens for a new header value sent as a part of some ping, updating the source of ExtensionConfigContext.GetWebhookUrl() with the new value once the slot swap is complete. This PR makes sure we always fetch the value from the ExtensionConfigContext instead of caching it to ensure we have the most up-to-date value.
1 parent 2073f7a commit 752d78f

10 files changed

+83
-71
lines changed

release_notes.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
## New Features
22
- Added support to select a storage backend provider when multiple are installed (#1702): Select which storage backend to use by setting the `type` field under `durableTask/storageProvider` in host.json. If this field isn't set, then the storage backend will default to using Azure Storage.
3-
43
- Improved concurrency defaults for the App Service Consumption plan (https://github.com/Azure/azure-functions-durable-extension/pull/1706)
54

5+
## Bug Fixes:
6+
- Properly used update management API URLs after a successful slot swap (#1716)

src/WebJobs.Extensions.DurableTask/DurableTaskExtension.cs

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -310,13 +310,17 @@ void IExtensionConfigProvider.Initialize(ExtensionConfigContext context)
310310
// Throw if any of the configured options are invalid
311311
this.Options.Validate(this.nameResolver, this.TraceHelper);
312312

313-
// For 202 support
314-
if (this.Options.NotificationUrl == null)
315-
{
316313
#pragma warning disable CS0618 // Type or member is obsolete
317-
this.Options.NotificationUrl = context.GetWebhookHandler();
314+
315+
// Invoke webhook handler to make functions runtime register extension endpoints.
316+
context.GetWebhookHandler();
317+
318+
// This line ensure every time we need the webhook URI, we get it directly from the
319+
// function runtime, which has the most up-to-date knowledge about the site hostname.
320+
this.HttpApiHandler.RegisterWebhookProvider(
321+
this.Options.WebhookUriProviderOverride ??
322+
context.GetWebhookHandler);
318323
#pragma warning restore CS0618 // Type or member is obsolete
319-
}
320324

321325
this.TraceConfigurationSettings();
322326

src/WebJobs.Extensions.DurableTask/HttpApiHandler.cs

Lines changed: 14 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ internal class HttpApiHandler : IDisposable
7171
private readonly EndToEndTraceHelper traceHelper;
7272
private readonly DurableTaskOptions durableTaskOptions;
7373
private readonly DurableTaskExtension config;
74+
private Func<Uri> webhookUrlProvider;
7475

7576
public HttpApiHandler(
7677
EndToEndTraceHelper traceHelper,
@@ -83,6 +84,8 @@ public HttpApiHandler(
8384
this.durableTaskOptions = durableTaskOptions;
8485
this.traceHelper = traceHelper;
8586

87+
this.webhookUrlProvider = this.durableTaskOptions.WebhookUriProviderOverride;
88+
8689
// The listen URL must not include the path.
8790
this.localHttpListener = new LocalHttpListener(
8891
this.traceHelper,
@@ -118,6 +121,11 @@ internal HttpResponseMessage CreateCheckStatusResponse(
118121
httpManagementPayload.RestartPostUri);
119122
}
120123

124+
public void RegisterWebhookProvider(Func<Uri> webhookProvider)
125+
{
126+
this.webhookUrlProvider = webhookProvider;
127+
}
128+
121129
// /orchestrators/{functionName}/{instanceId?}
122130
private static TemplateMatcher GetStartOrchestrationRoute()
123131
{
@@ -264,13 +272,9 @@ public async Task<HttpResponseMessage> HandleRequestAsync(HttpRequestMessage req
264272
{
265273
basePath = this.localHttpListener.InternalRpcUri.AbsolutePath;
266274
}
267-
else if (this.durableTaskOptions.NotificationUrl != null)
268-
{
269-
basePath = this.durableTaskOptions.NotificationUrl.AbsolutePath;
270-
}
271275
else
272276
{
273-
throw new InvalidOperationException($"Don't know how to handle request to {request.RequestUri}.");
277+
basePath = this.GetWebhookUri().AbsolutePath;
274278
}
275279

276280
string path = "/" + request.RequestUri.AbsolutePath.Substring(basePath.Length).Trim('/');
@@ -1021,19 +1025,15 @@ protected virtual IDurableClient GetClient(DurableClientAttribute attribute)
10211025

10221026
internal string GetBaseUrl()
10231027
{
1024-
this.ThrowIfWebhooksNotConfigured();
1025-
1026-
Uri notificationUri = this.durableTaskOptions.NotificationUrl;
1028+
Uri notificationUri = this.GetWebhookUri();
10271029

10281030
string hostUrl = notificationUri.GetLeftPart(UriPartial.Authority);
10291031
return hostUrl + notificationUri.AbsolutePath.TrimEnd('/');
10301032
}
10311033

10321034
internal string GetUniversalQueryStrings()
10331035
{
1034-
this.ThrowIfWebhooksNotConfigured();
1035-
1036-
Uri notificationUri = this.durableTaskOptions.NotificationUrl;
1036+
Uri notificationUri = this.GetWebhookUri();
10371037

10381038
return !string.IsNullOrEmpty(notificationUri.Query)
10391039
? notificationUri.Query.TrimStart('?')
@@ -1063,9 +1063,7 @@ private HttpManagementPayload GetClientResponseLinks(
10631063
string connectionName,
10641064
bool returnInternalServerErrorOnFailure = false)
10651065
{
1066-
this.ThrowIfWebhooksNotConfigured();
1067-
1068-
Uri notificationUri = this.durableTaskOptions.NotificationUrl;
1066+
Uri notificationUri = this.GetWebhookUri();
10691067
Uri baseUri = request?.RequestUri ?? notificationUri;
10701068

10711069
// e.g. http://{host}/runtime/webhooks/durabletask?code={systemKey}
@@ -1130,12 +1128,9 @@ private HttpResponseMessage CreateCheckStatusResponseMessage(
11301128
return response;
11311129
}
11321130

1133-
private void ThrowIfWebhooksNotConfigured()
1131+
private Uri GetWebhookUri()
11341132
{
1135-
if (this.durableTaskOptions.NotificationUrl == null)
1136-
{
1137-
throw new InvalidOperationException("Webhooks are not configured");
1138-
}
1133+
return this.webhookUrlProvider?.Invoke() ?? throw new InvalidOperationException("Webhooks are not configured");
11391134
}
11401135

11411136
internal bool TryGetRpcBaseUrl(out Uri rpcBaseUrl)

src/WebJobs.Extensions.DurableTask/Microsoft.Azure.WebJobs.Extensions.DurableTask-net461.xml

Lines changed: 1 addition & 11 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/WebJobs.Extensions.DurableTask/Microsoft.Azure.WebJobs.Extensions.DurableTask.xml

Lines changed: 1 addition & 11 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/WebJobs.Extensions.DurableTask/Options/DurableTaskOptions.cs

Lines changed: 4 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -98,17 +98,6 @@ public string HubName
9898
/// </value>
9999
public int? MaxConcurrentOrchestratorFunctions { get; set; } = null;
100100

101-
/// <summary>
102-
/// Gets or sets the base URL for the HTTP APIs managed by this extension.
103-
/// </summary>
104-
/// <remarks>
105-
/// This property is intended for use only by runtime hosts.
106-
/// </remarks>
107-
/// <value>
108-
/// A URL pointing to the hosted function app that responds to status polling requests.
109-
/// </value>
110-
public Uri NotificationUrl { get; set; }
111-
112101
/// <summary>
113102
/// Gets or sets a value indicating whether to enable the local RPC endpoint managed by this extension.
114103
/// </summary>
@@ -210,6 +199,10 @@ public string HubName
210199
// Used for mocking the lifecycle notification helper.
211200
internal HttpMessageHandler NotificationHandler { get; set; }
212201

202+
// This is just a way for tests to overwrite the webhook url, since there is no easy way
203+
// to mock the value from ExtensionConfigContext. It should not be used in production code paths.
204+
internal Func<Uri> WebhookUriProviderOverride { get; set; }
205+
213206
/// <summary>
214207
/// Sets HubName to a value that is considered a default value.
215208
/// </summary>
@@ -226,14 +219,6 @@ internal void TraceConfiguration(EndToEndTraceHelper traceHelper, JObject storag
226219
// We make updates to the clone rather than to JSON to ensure we're updating what we think we're updating.
227220
DurableTaskOptions clone = JObject.FromObject(this).ToObject<DurableTaskOptions>();
228221

229-
// Don't trace the notification URL query string since it may contain secrets.
230-
// This is the only property which we expect to contain secrets. Everything else should be *names*
231-
// of secrets that are resolved later from environment variables, etc.
232-
if (clone.NotificationUrl != null)
233-
{
234-
clone.NotificationUrl = new Uri(clone.NotificationUrl.GetLeftPart(UriPartial.Path));
235-
}
236-
237222
// At this stage the task hub name is expected to have been resolved. However, we want to know
238223
// what the original value was in addition to the resolved value, so we're updating the JSON
239224
// blob property to use the original, unresolved value.

test/Common/DurableClientBaseTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -322,7 +322,7 @@ private static DurableTaskExtension GetDurableTaskConfig()
322322
{
323323
var options = new DurableTaskOptions();
324324
options.HubName = "DurableTaskHub";
325-
options.NotificationUrl = new Uri("https://sampleurl.net");
325+
options.WebhookUriProviderOverride = () => new Uri("https://sampleurl.net");
326326
var wrappedOptions = new OptionsWrapper<DurableTaskOptions>(options);
327327
var nameResolver = TestHelpers.GetTestNameResolver();
328328
var connectionStringResolver = new TestConnectionStringResolver();

test/Common/HttpApiHandlerTests.cs

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,10 @@ public void CreateCheckStatusResponse_Throws_Exception_When_NotificationUrl_Miss
3333
{
3434
Notifications = new NotificationOptions(),
3535
};
36-
options.NotificationUrl = null;
36+
37+
// With a null override, and the production code path returning null webhook path,
38+
// this simulates a non-configured webhook url.
39+
options.WebhookUriProviderOverride = null;
3740
options.HubName = "DurableTaskHub";
3841

3942
var httpApiHandler = new HttpApiHandler(GetTestExtension(options), null);
@@ -62,6 +65,45 @@ public async Task WaitForCompletionOrCreateCheckStatusResponseAsync_Throws_Excep
6265
Assert.Equal($"Total timeout 0 should be bigger than retry timeout 100", ex.Message);
6366
}
6467

68+
[Fact]
69+
[Trait("Category", PlatformSpecificHelpers.TestCategory)]
70+
public void OutOfProcEndpoints_UpdateWithNewWebhookUri()
71+
{
72+
var httpApiHandler = new HttpApiHandler(GetTestExtension(), null);
73+
var webhookProvider = new ChangingWebhookProvider() { WebhookUri = new Uri(TestConstants.NotificationUrl) };
74+
httpApiHandler.RegisterWebhookProvider(() => webhookProvider.WebhookUri);
75+
76+
AssertApisUsingCorrectWebhookUri(httpApiHandler, TestConstants.NotificationUrlBase);
77+
78+
string newWebhookUri = TestConstants.NotificationUrl.Replace("localhost:7071", "localhost:5050");
79+
string newBaseUri = TestConstants.NotificationUrlBase.Replace("localhost:7071", "localhost:5050");
80+
webhookProvider.WebhookUri = new Uri(newWebhookUri);
81+
82+
AssertApisUsingCorrectWebhookUri(httpApiHandler, newBaseUri);
83+
}
84+
85+
// Validate the expected uris are used for CreateHttpManagementPayload(), GetBaseUrl(), and GetInstanceCreationLinks()
86+
private static void AssertApisUsingCorrectWebhookUri(HttpApiHandler httpApiHandler, string expectedBaseUri)
87+
{
88+
HttpManagementPayload managementPayload = httpApiHandler.CreateHttpManagementPayload(
89+
TestConstants.InstanceId,
90+
null,
91+
null);
92+
93+
Assert.StartsWith(expectedBaseUri, managementPayload.StatusQueryGetUri);
94+
Assert.StartsWith(expectedBaseUri, managementPayload.SendEventPostUri);
95+
Assert.StartsWith(expectedBaseUri, managementPayload.PurgeHistoryDeleteUri);
96+
Assert.StartsWith(expectedBaseUri, managementPayload.RestartPostUri);
97+
Assert.StartsWith(expectedBaseUri, managementPayload.TerminatePostUri);
98+
99+
string baseUri = httpApiHandler.GetBaseUrl();
100+
Assert.Equal(expectedBaseUri, baseUri);
101+
102+
HttpCreationPayload creationPayload = httpApiHandler.GetInstanceCreationLinks();
103+
Assert.StartsWith(expectedBaseUri, creationPayload.CreateNewInstancePostUri);
104+
Assert.StartsWith(expectedBaseUri, creationPayload.CreateAndWaitOnNewInstancePostUri);
105+
}
106+
65107
[Fact]
66108
[Trait("Category", PlatformSpecificHelpers.TestCategory)]
67109
public async Task CreateCheckStatusResponse_Returns_Correct_HTTP_202_Response()
@@ -1354,7 +1396,7 @@ public async Task SignalEntity_Is_Success(bool hasKey, bool hasOp, bool hasConte
13541396
private static DurableTaskExtension GetTestExtension()
13551397
{
13561398
var options = new DurableTaskOptions();
1357-
options.NotificationUrl = new Uri(TestConstants.NotificationUrl);
1399+
options.WebhookUriProviderOverride = () => new Uri(TestConstants.NotificationUrl);
13581400
options.HubName = "DurableFunctionsHub";
13591401

13601402
return GetTestExtension(options);
@@ -1382,6 +1424,11 @@ protected override IDurableClient GetClient(DurableClientAttribute attribute)
13821424
}
13831425
}
13841426

1427+
private class ChangingWebhookProvider
1428+
{
1429+
public Uri WebhookUri { get; set; }
1430+
}
1431+
13851432
private class MockDurableTaskExtension : DurableTaskExtension
13861433
{
13871434
public MockDurableTaskExtension(DurableTaskOptions options)

test/Common/TestHelpers.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ public static ITestHost GetJobHost(
104104
{
105105
DefaultAsyncRequestSleepTimeMilliseconds = httpAsyncSleepTime,
106106
};
107-
options.NotificationUrl = notificationUrl;
107+
options.WebhookUriProviderOverride = () => notificationUrl;
108108
options.ExtendedSessionsEnabled = enableExtendedSessions;
109109
options.MaxConcurrentOrchestratorFunctions = 200;
110110
options.MaxConcurrentActivityFunctions = 200;

test/FunctionsV2/DurableTaskListenerTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ private static DurableTaskExtension GetDurableTaskConfig()
5454
{
5555
var options = new DurableTaskOptions();
5656
options.HubName = "DurableTaskHub";
57-
options.NotificationUrl = new Uri("https://sampleurl.net");
57+
options.WebhookUriProviderOverride = () => new Uri("https://sampleurl.net");
5858
var wrappedOptions = new OptionsWrapper<DurableTaskOptions>(options);
5959
var nameResolver = TestHelpers.GetTestNameResolver();
6060
var connectionStringResolver = new TestConnectionStringResolver();

0 commit comments

Comments
 (0)