Skip to content

Commit 40890ab

Browse files
committed
Make App Offline file management more robust
1 parent a2d2308 commit 40890ab

File tree

12 files changed

+141
-22
lines changed

12 files changed

+141
-22
lines changed
Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,26 @@
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;
64
using System.IO;
7-
using System.Linq;
85
using System.Threading.Tasks;
96
using Microsoft.AspNetCore.Http;
7+
using Microsoft.Azure.WebJobs.Script.Extensions;
108

119
namespace Microsoft.Azure.WebJobs.Script.WebHost.Extensions
1210
{
1311
internal static class HttpContextExtensions
1412
{
1513
public static async Task SetOfflineResponseAsync(this HttpContext httpContext, string scriptPath)
1614
{
17-
// host is offline so return the app_offline.htm file content
18-
var offlineFilePath = Path.Combine(scriptPath, ScriptConstants.AppOfflineFileName);
19-
httpContext.Response.ContentType = "text/html";
2015
httpContext.Response.StatusCode = StatusCodes.Status503ServiceUnavailable;
21-
await httpContext.Response.SendFileAsync(offlineFilePath);
16+
17+
if (!httpContext.Request.IsAppServiceInternalRequest())
18+
{
19+
// host is offline so return the app_offline.htm file content
20+
var offlineFilePath = Path.Combine(scriptPath, ScriptConstants.AppOfflineFileName);
21+
httpContext.Response.ContentType = "text/html";
22+
await httpContext.Response.SendFileAsync(offlineFilePath);
23+
}
2224
}
2325
}
2426
}

src/WebJobs.Script.WebHost/FileMonitoringService.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -263,7 +263,13 @@ internal static async Task SetAppOfflineState(string rootPath, bool offline)
263263
else if (!offline && offlineFileExists)
264264
{
265265
// delete the app_offline.htm file
266-
FileUtility.DeleteFileSafe(path);
266+
await Utility.InvokeWithRetriesAsync(() =>
267+
{
268+
if (File.Exists(path))
269+
{
270+
File.Delete(path);
271+
}
272+
}, maxRetries: 3, retryInterval: TimeSpan.FromSeconds(1));
267273
}
268274
}
269275
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ public async Task Invoke(HttpContext context)
4040
{
4141
IActionResult result = null;
4242

43-
if (IsHomepageDisabled || context.Request.IsAntaresInternalRequest())
43+
if (IsHomepageDisabled || context.Request.IsAppServiceInternalRequest())
4444
{
4545
result = new NoContentResult();
4646
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ public async Task WarmUp(HttpRequest request)
4848
public static bool IsWarmUpRequest(HttpRequest request, IScriptWebHostEnvironment webHostEnvironment, IEnvironment environment)
4949
{
5050
return webHostEnvironment.InStandbyMode &&
51-
((environment.IsAppServiceEnvironment() && request.IsAntaresInternalRequest(environment)) || environment.IsLinuxContainerEnvironment()) &&
51+
((environment.IsAppServiceEnvironment() && request.IsAppServiceInternalRequest(environment)) || environment.IsLinuxContainerEnvironment()) &&
5252
(request.Path.StartsWithSegments(new PathString($"/api/{WarmUpConstants.FunctionName}")) ||
5353
request.Path.StartsWithSegments(new PathString($"/api/{WarmUpConstants.AlternateRoute}")));
5454
}

src/WebJobs.Script.WebHost/Security/Authorization/Policies/AuthorizationOptionsExtensions.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ public static void AddScriptPolicies(this AuthorizationOptions options)
3535
{
3636
if (c.Resource is AuthorizationFilterContext filterContext)
3737
{
38-
if (filterContext.HttpContext.Request.IsAntaresInternalRequest())
38+
if (filterContext.HttpContext.Request.IsAppServiceInternalRequest())
3939
{
4040
return true;
4141
}

src/WebJobs.Script/Extensions/FileUtility.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,9 @@ public static IFileSystem Instance
2222
set { _instance = value; }
2323
}
2424

25-
public static string ReadResourceString(string resourcePath)
25+
public static string ReadResourceString(string resourcePath, Assembly assembly = null)
2626
{
27-
Assembly assembly = Assembly.GetCallingAssembly();
27+
assembly = assembly ?? Assembly.GetCallingAssembly();
2828
using (StreamReader reader = new StreamReader(assembly.GetManifestResourceStream(resourcePath)))
2929
{
3030
return reader.ReadToEnd();

src/WebJobs.Script/Extensions/HttpRequestExtensions.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ public static TValue GetItemOrDefault<TValue>(this HttpRequest request, string k
5757
return default(TValue);
5858
}
5959

60-
public static bool IsAntaresInternalRequest(this HttpRequest request, IEnvironment environment = null)
60+
public static bool IsAppServiceInternalRequest(this HttpRequest request, IEnvironment environment = null)
6161
{
6262
environment = environment ?? SystemEnvironment.Instance;
6363
if (!environment.IsAppServiceEnvironment())

src/WebJobs.Script/Utility.cs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,36 @@ public static class Utility
3434
private static readonly FilteredExpandoObjectConverter _filteredExpandoObjectConverter = new FilteredExpandoObjectConverter();
3535
private static List<string> dotNetLanguages = new List<string>() { DotNetScriptTypes.CSharp, DotNetScriptTypes.DotNetAssembly };
3636

37+
internal static async Task InvokeWithRetriesAsync(Action action, int maxRetries, TimeSpan retryInterval)
38+
{
39+
await InvokeWithRetriesAsync(() =>
40+
{
41+
action();
42+
return Task.CompletedTask;
43+
}, maxRetries, retryInterval);
44+
}
45+
46+
internal static async Task InvokeWithRetriesAsync(Func<Task> action, int maxRetries, TimeSpan retryInterval)
47+
{
48+
int attempt = 0;
49+
while (true)
50+
{
51+
try
52+
{
53+
await action();
54+
return;
55+
}
56+
catch (Exception ex) when (!ex.IsFatal())
57+
{
58+
if (++attempt > maxRetries)
59+
{
60+
throw;
61+
}
62+
await Task.Delay(retryInterval);
63+
}
64+
}
65+
}
66+
3767
/// <summary>
3868
/// Delays while the specified condition remains true.
3969
/// </summary>
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
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.Collections.Generic;
5+
using System.IO;
6+
using System.Threading;
7+
using System.Threading.Tasks;
8+
using Microsoft.AspNetCore.Http;
9+
using Microsoft.AspNetCore.Http.Features;
10+
using Microsoft.Azure.WebJobs.Script.Extensions;
11+
using Microsoft.Azure.WebJobs.Script.WebHost;
12+
using Microsoft.Azure.WebJobs.Script.WebHost.Extensions;
13+
using Microsoft.Extensions.Primitives;
14+
using Moq;
15+
using Xunit;
16+
17+
namespace Microsoft.Azure.WebJobs.Script.Tests.Extensions
18+
{
19+
public class HttpContextExtensionsTests
20+
{
21+
[Fact]
22+
public async Task SetOfflineResponseAsync_ReturnsCorrectResponse()
23+
{
24+
// create a test offline file
25+
var rootPath = Path.GetTempPath();
26+
var offlineFilePath = Path.Combine(Path.GetTempPath(), ScriptConstants.AppOfflineFileName);
27+
string content = FileUtility.ReadResourceString($"{ScriptConstants.ResourcePath}.{ScriptConstants.AppOfflineFileName}", typeof(HttpException).Assembly);
28+
File.WriteAllText(offlineFilePath, content);
29+
30+
var sendFileFeatureMock = new Mock<IHttpSendFileFeature>();
31+
sendFileFeatureMock.Setup(s => s.SendFileAsync(offlineFilePath, 0, null, CancellationToken.None)).Returns(Task.FromResult<int>(0));
32+
33+
// simulate an App Service request
34+
var vars = new Dictionary<string, string>
35+
{
36+
{ EnvironmentSettingNames.AzureWebsiteInstanceId, "123" }
37+
};
38+
using (var env = new TestScopedEnvironmentVariable(vars))
39+
{
40+
// without header (thus an internal request)
41+
var context = new DefaultHttpContext();
42+
context.Features.Set(sendFileFeatureMock.Object);
43+
Assert.True(context.Request.IsAppServiceInternalRequest());
44+
await context.SetOfflineResponseAsync(rootPath);
45+
Assert.Equal(StatusCodes.Status503ServiceUnavailable, context.Response.StatusCode);
46+
Assert.Equal(0, context.Response.Body.Length);
47+
sendFileFeatureMock.Verify(p => p.SendFileAsync(offlineFilePath, 0, null, CancellationToken.None), Times.Never);
48+
49+
// with header (thus an external request)
50+
context = new DefaultHttpContext();
51+
context.Features.Set(sendFileFeatureMock.Object);
52+
context.Request.Headers.Add(ScriptConstants.AntaresLogIdHeaderName, new StringValues("456"));
53+
Assert.False(context.Request.IsAppServiceInternalRequest());
54+
await context.SetOfflineResponseAsync(rootPath);
55+
Assert.Equal(StatusCodes.Status503ServiceUnavailable, context.Response.StatusCode);
56+
Assert.Equal("text/html", context.Response.Headers["Content-Type"]);
57+
sendFileFeatureMock.Verify(p => p.SendFileAsync(offlineFilePath, 0, null, CancellationToken.None), Times.Once);
58+
}
59+
}
60+
}
61+
}

test/WebJobs.Script.Tests/Extensions/HttpRequestExtensionsTest.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ public void IsAntaresInternalRequest_ReturnsExpectedResult()
1919
{
2020
// not running under Azure
2121
var request = HttpTestHelpers.CreateHttpRequest("GET", "http://foobar");
22-
Assert.False(request.IsAntaresInternalRequest());
22+
Assert.False(request.IsAppServiceInternalRequest());
2323

2424
// running under Azure
2525
var vars = new Dictionary<string, string>
@@ -33,10 +33,10 @@ public void IsAntaresInternalRequest_ReturnsExpectedResult()
3333
headers.Add(ScriptConstants.AntaresLogIdHeaderName, "123");
3434

3535
request = HttpTestHelpers.CreateHttpRequest("GET", "http://foobar", headers);
36-
Assert.False(request.IsAntaresInternalRequest());
36+
Assert.False(request.IsAppServiceInternalRequest());
3737

3838
request = HttpTestHelpers.CreateHttpRequest("GET", "http://foobar");
39-
Assert.True(request.IsAntaresInternalRequest());
39+
Assert.True(request.IsAppServiceInternalRequest());
4040
}
4141
}
4242
}

0 commit comments

Comments
 (0)