Skip to content

Commit 5eec1fe

Browse files
brettsammhoeger
authored andcommitted
protecting against IOExceptions when flushing FileWriter
1 parent a47d5b6 commit 5eec1fe

File tree

14 files changed

+233
-20
lines changed

14 files changed

+233
-20
lines changed
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
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+
namespace Microsoft.Azure.WebJobs.Script
5+
{
6+
internal class DefaultFileWriterFactory : IFileWriterFactory
7+
{
8+
/// <summary>
9+
/// Creates an IFileWriter. The caller is responsible for disposing of the IFileWriter.
10+
/// </summary>
11+
/// <param name="filePath">The file path.</param>
12+
/// <returns>An IFileWriter.</returns>
13+
public IFileWriter Create(string filePath) => new FileWriter(filePath);
14+
}
15+
}

src/WebJobs.Script/Diagnostics/FileLogger.cs

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,14 @@ namespace Microsoft.Azure.WebJobs.Script.Diagnostics
1212
{
1313
internal class FileLogger : ILogger
1414
{
15-
private readonly FileWriter _fileWriter;
15+
private readonly IFileWriter _fileWriter;
1616
private readonly Func<bool> _isFileLoggingEnabled;
1717
private readonly Func<bool> _isPrimary;
1818
private readonly string _categoryName;
1919
private readonly LogType _logType;
2020
private readonly IExternalScopeProvider _scopeProvider;
2121

22-
public FileLogger(string categoryName, FileWriter fileWriter, Func<bool> isFileLoggingEnabled, Func<bool> isPrimary, LogType logType, IExternalScopeProvider scopeProvider)
22+
public FileLogger(string categoryName, IFileWriter fileWriter, Func<bool> isFileLoggingEnabled, Func<bool> isPrimary, LogType logType, IExternalScopeProvider scopeProvider)
2323
{
2424
_fileWriter = fileWriter;
2525
_isFileLoggingEnabled = isFileLoggingEnabled;
@@ -88,12 +88,20 @@ public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Except
8888
}
8989

9090
formattedMessage = FormatLine(stateValues, logLevel, formattedMessage);
91-
_fileWriter.AppendLine(formattedMessage);
9291

93-
// flush errors immediately
94-
if (logLevel == LogLevel.Error || exception != null)
92+
try
9593
{
96-
_fileWriter.Flush();
94+
_fileWriter.AppendLine(formattedMessage);
95+
96+
// flush errors immediately
97+
if (logLevel == LogLevel.Error || exception != null)
98+
{
99+
_fileWriter.Flush();
100+
}
101+
}
102+
catch (Exception)
103+
{
104+
// Make sure the Logger doesn't throw if there are Exceptions (disk full, etc).
97105
}
98106
}
99107

src/WebJobs.Script/Diagnostics/FileWriter.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313

1414
namespace Microsoft.Azure.WebJobs.Script
1515
{
16-
internal class FileWriter : IDisposable
16+
internal class FileWriter : IFileWriter, IDisposable
1717
{
1818
internal const int LastModifiedCutoffDays = 1;
1919
internal const long MaxLogFileSizeBytes = 5 * 1024 * 1024;

src/WebJobs.Script/Diagnostics/FunctionFileLoggerProvider.cs

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,27 +18,28 @@ namespace Microsoft.Azure.WebJobs.Script.Diagnostics
1818
/// </summary>
1919
internal class FunctionFileLoggerProvider : ILoggerProvider, ISupportExternalScope
2020
{
21-
private readonly ConcurrentDictionary<string, FileWriter> _fileWriterCache = new ConcurrentDictionary<string, FileWriter>(StringComparer.OrdinalIgnoreCase);
21+
private readonly ConcurrentDictionary<string, IFileWriter> _fileWriterCache = new ConcurrentDictionary<string, IFileWriter>(StringComparer.OrdinalIgnoreCase);
2222
private readonly Func<bool> _isFileLoggingEnabled;
2323
private readonly Func<bool> _isPrimary;
2424
private readonly string _roogLogPath;
2525
private readonly string _hostInstanceId;
2626
private static readonly Regex _workerCategoryRegex = new Regex(@"^Worker\.[^\s]+\.[^\s]+");
27-
27+
private readonly IFileWriterFactory _fileWriterFactory;
2828
private IExternalScopeProvider _scopeProvider;
2929
private bool _disposed = false;
3030

3131
public FunctionFileLoggerProvider(IOptions<ScriptJobHostOptions> scriptOptions, IFileLoggingStatusManager fileLoggingStatusManager,
32-
IPrimaryHostStateProvider primaryHostStateProvider)
32+
IPrimaryHostStateProvider primaryHostStateProvider, IFileWriterFactory fileWriterFactory)
3333
{
3434
_roogLogPath = scriptOptions.Value.RootLogPath;
3535
_isFileLoggingEnabled = () => fileLoggingStatusManager.IsFileLoggingEnabled;
3636
_isPrimary = () => primaryHostStateProvider.IsPrimary;
3737
_hostInstanceId = scriptOptions.Value.InstanceId;
38+
_fileWriterFactory = fileWriterFactory ?? throw new ArgumentNullException(nameof(fileWriterFactory));
3839
}
3940

4041
// For testing
41-
internal IDictionary<string, FileWriter> FileWriterCache => _fileWriterCache;
42+
internal IDictionary<string, IFileWriter> FileWriterCache => _fileWriterCache;
4243

4344
public ILogger CreateLogger(string categoryName)
4445
{
@@ -48,7 +49,7 @@ public ILogger CreateLogger(string categoryName)
4849
{
4950
// Make sure that we return the same fileWriter if multiple loggers write to the same path. This happens
5051
// with Function logs as Function.{FunctionName} and Function.{FunctionName}.User both go to the same file.
51-
FileWriter fileWriter = _fileWriterCache.GetOrAdd(filePath, (p) => new FileWriter(Path.Combine(_roogLogPath, filePath)));
52+
IFileWriter fileWriter = _fileWriterCache.GetOrAdd(filePath, (p) => _fileWriterFactory.Create(Path.Combine(_roogLogPath, filePath)));
5253
return new FileLogger(categoryName, fileWriter, _isFileLoggingEnabled, _isPrimary, LogType.Function, _scopeProvider);
5354
}
5455

@@ -85,9 +86,9 @@ public void Dispose()
8586
{
8687
foreach (string fileWriterKey in _fileWriterCache.Keys)
8788
{
88-
if (_fileWriterCache.TryRemove(fileWriterKey, out FileWriter fileWriter))
89+
if (_fileWriterCache.TryRemove(fileWriterKey, out IFileWriter fileWriter))
8990
{
90-
fileWriter.Dispose();
91+
(fileWriter as IDisposable)?.Dispose();
9192
}
9293
}
9394

src/WebJobs.Script/Diagnostics/HostFileLoggerProvider.cs

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,20 @@ namespace Microsoft.Azure.WebJobs.Script.Diagnostics
1313
/// </summary>
1414
internal class HostFileLoggerProvider : ILoggerProvider, ISupportExternalScope
1515
{
16-
private readonly FileWriter _writer;
16+
private readonly IFileWriter _writer;
1717
private readonly Func<bool> _isFileLoggingEnabled;
1818

1919
private bool _disposed = false;
2020
private IExternalScopeProvider _scopeProvider;
2121

22-
public HostFileLoggerProvider(IOptions<ScriptJobHostOptions> options, IFileLoggingStatusManager fileLoggingStatusManager)
22+
public HostFileLoggerProvider(IOptions<ScriptJobHostOptions> options, IFileLoggingStatusManager fileLoggingStatusManager, IFileWriterFactory fileWriterFactory)
2323
{
24-
_writer = new FileWriter(Path.Combine(options.Value.RootLogPath, "Host"));
24+
if (fileWriterFactory == null)
25+
{
26+
throw new ArgumentNullException(nameof(fileWriterFactory));
27+
}
28+
29+
_writer = fileWriterFactory.Create(Path.Combine(options.Value.RootLogPath, "Host"));
2530
_isFileLoggingEnabled = () => fileLoggingStatusManager.IsFileLoggingEnabled;
2631
}
2732

@@ -39,7 +44,7 @@ public void Dispose()
3944
{
4045
if (!_disposed)
4146
{
42-
_writer.Dispose();
47+
(_writer as IDisposable)?.Dispose();
4348
_disposed = true;
4449
}
4550
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
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+
namespace Microsoft.Azure.WebJobs.Script
5+
{
6+
internal interface IFileWriter
7+
{
8+
void AppendLine(string line);
9+
10+
void Flush();
11+
}
12+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
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+
namespace Microsoft.Azure.WebJobs.Script
5+
{
6+
internal interface IFileWriterFactory
7+
{
8+
IFileWriter Create(string filePath);
9+
}
10+
}

src/WebJobs.Script/ScriptHostBuilderExtensions.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ public static IHostBuilder AddScriptHost(this IHostBuilder builder,
7878
string loggingPath = ConfigurationPath.Combine(ConfigurationSectionNames.JobHost, "Logging");
7979
loggingBuilder.AddConfiguration(context.Configuration.GetSection(loggingPath));
8080

81+
loggingBuilder.Services.AddSingleton<IFileWriterFactory, DefaultFileWriterFactory>();
8182
loggingBuilder.Services.AddSingleton<ILoggerProvider, HostFileLoggerProvider>();
8283
loggingBuilder.Services.AddSingleton<ILoggerProvider, FunctionFileLoggerProvider>();
8384

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
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 System.IO;
6+
using System.Linq;
7+
using System.Net;
8+
using System.Net.Http;
9+
using System.Threading.Tasks;
10+
using Microsoft.Azure.WebJobs.Script.WebHost.Diagnostics;
11+
using Microsoft.Azure.WebJobs.Script.WebHost.Models;
12+
using Microsoft.Extensions.DependencyInjection;
13+
using Newtonsoft.Json;
14+
using Xunit;
15+
16+
namespace Microsoft.Azure.WebJobs.Script.Tests.Integration.WebHostEndToEnd
17+
{
18+
public class DiagnosticsEndToEndTests
19+
{
20+
private const string _scriptRoot = @"TestScripts\CSharp";
21+
private readonly string _testLogPath = Path.Combine(TestHelpers.FunctionsTestDirectory, "Logs", Guid.NewGuid().ToString(), @"Functions");
22+
23+
private TestEventGenerator _eventGenerator = new TestEventGenerator();
24+
25+
[Fact]
26+
public async Task FileLogger_IOExceptionDuringInvocation_Recovers()
27+
{
28+
var fileWriterFactory = new TestFileWriterFactory(onAppendLine: null,
29+
onFlush: () =>
30+
{
31+
// The below function will fail, causing an immediate flush. This exception
32+
// simulates the disk being full. ExecutionEvents should be logged as expected
33+
// and the "Finished" event should get logged.
34+
throw new IOException();
35+
});
36+
37+
using (var host = new TestFunctionHost(_scriptRoot, _testLogPath,
38+
configureWebHostServices: s =>
39+
{
40+
s.AddSingleton<IEventGenerator>(_ => _eventGenerator);
41+
},
42+
configureScriptHostServices: s =>
43+
{
44+
s.AddSingleton<IFileWriterFactory>(_ => fileWriterFactory);
45+
46+
s.PostConfigure<ScriptJobHostOptions>(o =>
47+
{
48+
o.FileLoggingMode = FileLoggingMode.Always;
49+
o.Functions = new[] { "HttpTrigger-Scenarios" };
50+
});
51+
}))
52+
{
53+
// Issue an invalid request that fails.
54+
var content = new StringContent(JsonConvert.SerializeObject(new { scenario = "invalid" }));
55+
var response = await host.HttpClient.PostAsync("/api/HttpTrigger-Scenarios", content);
56+
Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode);
57+
58+
await TestHelpers.Await(() =>
59+
{
60+
var executionEvents = _eventGenerator.GetFunctionExecutionEvents();
61+
return executionEvents.SingleOrDefault(p => p.ExecutionStage == ExecutionStage.Finished) != null;
62+
});
63+
}
64+
}
65+
66+
private class TestFileWriterFactory : IFileWriterFactory
67+
{
68+
private readonly Action<string> _onAppendLine;
69+
private readonly Action _onFlush;
70+
71+
public TestFileWriterFactory(Action<string> onAppendLine, Action onFlush)
72+
{
73+
_onAppendLine = onAppendLine;
74+
_onFlush = onFlush;
75+
}
76+
77+
public IFileWriter Create(string filePath) => new TestFileWriter(_onAppendLine, _onFlush);
78+
}
79+
80+
private class TestFileWriter : IFileWriter
81+
{
82+
private readonly Action<string> _onAppendLine;
83+
private readonly Action _onFlush;
84+
85+
public TestFileWriter(Action<string> onAppendLine, Action onFlush)
86+
{
87+
_onAppendLine = onAppendLine;
88+
_onFlush = onFlush;
89+
}
90+
91+
public void AppendLine(string line)
92+
{
93+
_onAppendLine?.Invoke(line);
94+
}
95+
96+
public void Flush()
97+
{
98+
_onFlush();
99+
}
100+
}
101+
}
102+
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -278,7 +278,7 @@ private IWebHostBuilder CreateStandbyHostBuilder(params string[] functions)
278278
// request for triggering specialization.
279279
s.AddSingleton<IStandbyManager, InfiniteTimerStandbyManager>();
280280

281-
s.AddSingleton<IScriptHostBuilder, PausingScriptHostBuilder>();
281+
s.AddSingleton<IScriptHostBuilder, PausingScriptHostBuilder>();
282282
})
283283
.ConfigureScriptHostServices(s =>
284284
{

0 commit comments

Comments
 (0)