Skip to content

Commit e95487c

Browse files
committed
Concurrency improvements for FileTraceWriter
1 parent 8c8a1b4 commit e95487c

File tree

7 files changed

+237
-108
lines changed

7 files changed

+237
-108
lines changed

src/WebJobs.Script/Diagnostics/FileTraceWriter.cs

Lines changed: 103 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ namespace Microsoft.Azure.WebJobs.Script
1919
public class FileTraceWriter : TraceWriter, IDisposable
2020
{
2121
internal const int LastModifiedCutoffDays = 1;
22-
private const long MaxLogFileSizeBytes = 5 * 1024 * 1024;
22+
internal const long MaxLogFileSizeBytes = 5 * 1024 * 1024;
2323
internal const int LogFlushIntervalMs = 1000;
2424
internal const int MaxLogLinesPerFlushInterval = 250;
2525
private readonly string _logFilePath;
@@ -38,24 +38,7 @@ public FileTraceWriter(string logFilePath, TraceLevel level, LogType logType) :
3838
_logFilePath = logFilePath;
3939
_instanceId = GetInstanceId();
4040
_logType = logType;
41-
4241
_logDirectory = new DirectoryInfo(logFilePath);
43-
if (!_logDirectory.Exists)
44-
{
45-
_logDirectory.Create();
46-
}
47-
else
48-
{
49-
// query for all existing log files for this instance
50-
// sorted by date, and get the last log file written to (or null)
51-
var files = GetLogFiles(_logDirectory);
52-
_currentLogFileInfo = files.FirstOrDefault();
53-
}
54-
55-
if (_currentLogFileInfo == null)
56-
{
57-
SetNewLogFile();
58-
}
5942

6043
// start a timer to flush accumulated logs in batches
6144
_flushTimer = new Timer
@@ -100,30 +83,45 @@ public override void Flush()
10083
sb.AppendLine(line);
10184
}
10285

86+
if (_currentLogFileInfo == null)
87+
{
88+
// delay create log file
89+
SetLogFile();
90+
}
91+
10392
// write all lines in a single file operation
104-
string contents = sb.ToString();
93+
string content = sb.ToString();
10594
try
10695
{
107-
lock (_syncLock)
108-
{
109-
File.AppendAllText(_currentLogFileInfo.FullName, contents);
110-
}
96+
AppendToFile(_currentLogFileInfo, content);
11197
}
112-
catch (DirectoryNotFoundException)
98+
catch (Exception ex) when (ex is DirectoryNotFoundException || ex is FileNotFoundException)
11399
{
114-
// need to handle cases where log file directories might
100+
// need to handle cases where log files or directories might
115101
// have been deleted from underneath us
116-
Directory.CreateDirectory(_logFilePath);
117-
lock (_syncLock)
118-
{
119-
File.AppendAllText(_currentLogFileInfo.FullName, contents);
120-
}
102+
SetLogFile();
103+
AppendToFile(_currentLogFileInfo, content);
121104
}
122105

106+
// check to see if we need to roll to a new log file
123107
_currentLogFileInfo.Refresh();
124-
if (_currentLogFileInfo.Length > MaxLogFileSizeBytes)
108+
if (LogFileIsOverSize(_currentLogFileInfo))
125109
{
126-
SetNewLogFile();
110+
SetLogFile();
111+
}
112+
}
113+
114+
private static void AppendToFile(FileInfo fileInfo, string content)
115+
{
116+
lock (_syncLock)
117+
{
118+
var fs = fileInfo.Open(FileMode.Open, FileAccess.Write);
119+
fs.Seek(0, SeekOrigin.End);
120+
121+
using (var sw = new StreamWriter(fs))
122+
{
123+
sw.Write(content);
124+
}
127125
}
128126
}
129127

@@ -250,21 +248,84 @@ private void OnFlushLogs(object sender, ElapsedEventArgs e)
250248
Flush();
251249
}
252250

253-
internal void SetNewLogFile()
251+
/// <summary>
252+
/// Called to set/create the current log file that will be used. Also handles purging of old
253+
/// log files.
254+
/// </summary>
255+
internal void SetLogFile()
256+
{
257+
if (!_logDirectory.Exists)
258+
{
259+
_logDirectory.Create();
260+
}
261+
262+
// Check to see if there is already a log file we can use
263+
bool newLogFileCreated = false;
264+
var logFile = GetCurrentLogFile();
265+
if (logFile != null)
266+
{
267+
_currentLogFileInfo = logFile;
268+
}
269+
else
270+
{
271+
lock (_syncLock)
272+
{
273+
// we perform any file create operations in a lock to avoid
274+
// race conditions with multiple instances of this class on
275+
// multiple threads, where multiple log files might be created
276+
// unnecessarily
277+
logFile = GetCurrentLogFile();
278+
if (logFile != null)
279+
{
280+
_currentLogFileInfo = logFile;
281+
}
282+
else
283+
{
284+
// create a new log file
285+
// we include a machine identifier in the log file name to ensure we don't have any
286+
// log file contention between scaled out instances
287+
var timestamp = DateTime.UtcNow.ToString("yyyy-MM-ddTHH-mm-ssK");
288+
string filePath = Path.Combine(_logFilePath, $"{timestamp}-{_instanceId}.log");
289+
_currentLogFileInfo = new FileInfo(filePath);
290+
_currentLogFileInfo.Create().Close();
291+
newLogFileCreated = true;
292+
}
293+
}
294+
}
295+
296+
if (newLogFileCreated)
297+
{
298+
// purge any log files (regardless of instance ID) whose last write time was earlier
299+
// than our retention policy
300+
// only do this peridically when we create a new file which will tend to keep
301+
// this off the startup path
302+
var filesToPurge = _logDirectory.GetFiles("*.log").Where(p => LogFileIsOld(p));
303+
DeleteFiles(filesToPurge);
304+
}
305+
}
306+
307+
private static bool LogFileIsOverSize(FileInfo fileInfo)
308+
{
309+
// return true if the file is over our size threshold
310+
return fileInfo.Length > MaxLogFileSizeBytes;
311+
}
312+
313+
private static bool LogFileIsOld(FileInfo fileInfo)
314+
{
315+
return (DateTime.UtcNow - fileInfo.LastWriteTimeUtc).TotalDays > LastModifiedCutoffDays;
316+
}
317+
318+
private FileInfo GetCurrentLogFile()
254319
{
255-
// purge any log files (regardless of instance ID) whose last write time was earlier
256-
// than our retention policy
257-
var filesToPurge = _logDirectory.GetFiles("*.log").Where(p => (DateTime.UtcNow - p.LastWriteTimeUtc).TotalDays > LastModifiedCutoffDays);
258-
DeleteFiles(filesToPurge);
259-
260-
// we include a machine identifier in the log file name to ensure we don't have any
261-
// log file contention between scaled out instances
262-
string filePath = Path.Combine(_logFilePath, string.Format(CultureInfo.InvariantCulture, "{0}-{1}.log", DateTime.UtcNow.ToString("yyyy-MM-ddTHH-mm-ssK"), _instanceId));
263-
_currentLogFileInfo = new FileInfo(filePath);
320+
// return the last log file written to which is still under
321+
// size threshold
322+
return GetLogFiles(_logDirectory).FirstOrDefault(p => !LogFileIsOverSize(p));
264323
}
265324

266325
private IEnumerable<FileInfo> GetLogFiles(DirectoryInfo directory)
267326
{
327+
// query for all existing log files for this instance
328+
// sorted by date
268329
string pattern = string.Format(CultureInfo.InvariantCulture, "*-{0}.log", _instanceId);
269330
return directory.GetFiles(pattern).OrderByDescending(p => p.LastWriteTime);
270331
}

test/WebJobs.Script.Tests.Integration/ApiHubTestHelper.cs

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,6 @@ public static class ApiHubTestHelper
2525
private const string DataSetName = "default";
2626
private const string TableName = "SampleTable";
2727
private const string PrimaryKeyColumn = "Id";
28-
private const string Chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
29-
30-
private static readonly Random Random = new Random();
3128

3229
private static readonly ScriptSettingsManager SettingsManager = ScriptSettingsManager.Instance;
3330

@@ -61,14 +58,6 @@ public static void SetDefaultConnectionFactory()
6158
}
6259
}
6360

64-
public static string NewRandomString()
65-
{
66-
return new string(
67-
Enumerable.Repeat('x', 10) // nvarchar(10)
68-
.Select(c => Chars[Random.Next(Chars.Length)])
69-
.ToArray());
70-
}
71-
7261
public static async Task EnsureEntityAsync(int entityId, string text = null)
7362
{
7463
var connection = ConnectionFactory.Default.CreateConnection(Key);

test/WebJobs.Script.Tests.Integration/CSharpEndToEndTests.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,7 @@ public async Task ApiHub()
178178
[Fact]
179179
public async Task ApiHubTableClientBindingTest()
180180
{
181-
var textArgValue = ApiHubTestHelper.NewRandomString();
181+
var textArgValue = TestHelpers.NewRandomString();
182182

183183
// Ensure the test entity exists.
184184
await ApiHubTestHelper.EnsureEntityAsync(ApiHubTestHelper.EntityId1);
@@ -197,7 +197,7 @@ await ApiHubTestHelper.AssertTextUpdatedAsync(
197197
[Fact]
198198
public async Task ApiHubTableBindingTest()
199199
{
200-
var textArgValue = ApiHubTestHelper.NewRandomString();
200+
var textArgValue = TestHelpers.NewRandomString();
201201

202202
// Ensure the test entity exists.
203203
await ApiHubTestHelper.EnsureEntityAsync(ApiHubTestHelper.EntityId2);
@@ -221,7 +221,7 @@ await ApiHubTestHelper.AssertTextUpdatedAsync(
221221
[Fact]
222222
public async Task ApiHubTableEntityBindingTest()
223223
{
224-
var textArgValue = ApiHubTestHelper.NewRandomString();
224+
var textArgValue = TestHelpers.NewRandomString();
225225

226226
// Ensure the test entity exists.
227227
await ApiHubTestHelper.EnsureEntityAsync(ApiHubTestHelper.EntityId3);

test/WebJobs.Script.Tests.Integration/FSharpEndToEndTests.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ public async Task ApiHub()
113113
[Fact]
114114
public async Task ApiHubTableClientBindingTest()
115115
{
116-
var textArgValue = ApiHubTestHelper.NewRandomString();
116+
var textArgValue = TestHelpers.NewRandomString();
117117

118118
// Ensure the test entity exists.
119119
await ApiHubTestHelper.EnsureEntityAsync(ApiHubTestHelper.EntityId1);
@@ -132,7 +132,7 @@ await ApiHubTestHelper.AssertTextUpdatedAsync(
132132
[Fact]
133133
public async Task ApiHubTableBindingTest()
134134
{
135-
var textArgValue = ApiHubTestHelper.NewRandomString();
135+
var textArgValue = TestHelpers.NewRandomString();
136136

137137
// Ensure the test entity exists.
138138
await ApiHubTestHelper.EnsureEntityAsync(ApiHubTestHelper.EntityId2);
@@ -156,7 +156,7 @@ await ApiHubTestHelper.AssertTextUpdatedAsync(
156156
[Fact]
157157
public async Task ApiHubTableEntityBindingTest()
158158
{
159-
var textArgValue = ApiHubTestHelper.NewRandomString();
159+
var textArgValue = TestHelpers.NewRandomString();
160160

161161
// Ensure the test entity exists.
162162
await ApiHubTestHelper.EnsureEntityAsync(ApiHubTestHelper.EntityId3);

test/WebJobs.Script.Tests.Integration/NodeEndToEndTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1115,7 +1115,7 @@ await Fixture.Host.CallAsync("ApiHubTableEntityIn",
11151115
[Fact]
11161116
public async Task ApiHubTableEntityOut()
11171117
{
1118-
var textArgValue = ApiHubTestHelper.NewRandomString();
1118+
var textArgValue = TestHelpers.NewRandomString();
11191119

11201120
// Delete the test entity if it exists.
11211121
await ApiHubTestHelper.DeleteEntityAsync(ApiHubTestHelper.EntityId5);

test/WebJobs.Script.Tests.Shared/TestHelpers.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ namespace Microsoft.Azure.WebJobs.Script.Tests
2121
{
2222
public static class TestHelpers
2323
{
24+
private const string Chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
25+
private static readonly Random Random = new Random();
26+
2427
/// <summary>
2528
/// Gets the common root directory that functions tests create temporary directories under.
2629
/// This enables us to clean up test files by deleting this single directory.
@@ -33,6 +36,14 @@ public static string FunctionsTestDirectory
3336
}
3437
}
3538

39+
public static string NewRandomString(int length = 10)
40+
{
41+
return new string(
42+
Enumerable.Repeat('x', length)
43+
.Select(c => Chars[Random.Next(Chars.Length)])
44+
.ToArray());
45+
}
46+
3647
public static async Task Await(Func<bool> condition, int timeout = 60 * 1000, int pollingInterval = 2 * 1000, bool throwWhenDebugging = false, string userMessage = null)
3748
{
3849
DateTime start = DateTime.Now;

0 commit comments

Comments
 (0)