Skip to content

Commit 4307d60

Browse files
committed
Fixing bug in log file purge logic (#393)
1 parent 94db5f6 commit 4307d60

File tree

7 files changed

+196
-54
lines changed

7 files changed

+196
-54
lines changed

src/WebJobs.Script.WebHost/SecretManager.cs

Lines changed: 38 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
using System.IO;
88
using System.Linq;
99
using System.Security.Cryptography;
10+
using Microsoft.Azure.WebJobs.Host;
1011
using Newtonsoft.Json;
1112

1213
namespace Microsoft.Azure.WebJobs.Script.WebHost
@@ -107,43 +108,57 @@ public virtual FunctionSecrets GetFunctionSecrets(string functionName)
107108
/// to a function.
108109
/// </summary>
109110
/// <param name="rootScriptPath">The root function directory.</param>
110-
public void PurgeOldFiles(string rootScriptPath)
111+
/// <param name="traceWriter">The TraceWriter to log to.</param>
112+
public void PurgeOldFiles(string rootScriptPath, TraceWriter traceWriter)
111113
{
112-
if (!Directory.Exists(rootScriptPath))
114+
try
113115
{
114-
return;
115-
}
116+
if (!Directory.Exists(rootScriptPath))
117+
{
118+
return;
119+
}
116120

117-
// Create a lookup of all potential functions (whether they're valid or not)
118-
// It is important that we determine functions based on the presence of a folder,
119-
// not whether we've identified a valid function from that folder. This ensures
120-
// that we don't delete logs/secrets for functions that transition into/out of
121-
// invalid unparsable states.
122-
var functionLookup = Directory.EnumerateDirectories(rootScriptPath).ToLookup(p => Path.GetFileName(p), StringComparer.OrdinalIgnoreCase);
121+
// Create a lookup of all potential functions (whether they're valid or not)
122+
// It is important that we determine functions based on the presence of a folder,
123+
// not whether we've identified a valid function from that folder. This ensures
124+
// that we don't delete logs/secrets for functions that transition into/out of
125+
// invalid unparsable states.
126+
var functionLookup = Directory.EnumerateDirectories(rootScriptPath).ToLookup(p => Path.GetFileName(p), StringComparer.OrdinalIgnoreCase);
123127

124-
var secretsDirectory = new DirectoryInfo(_secretsPath);
125-
foreach (var secretFile in secretsDirectory.GetFiles("*.json"))
126-
{
127-
if (string.Compare(secretFile.Name, ScriptConstants.HostMetadataFileName, StringComparison.OrdinalIgnoreCase) == 0)
128+
var secretsDirectory = new DirectoryInfo(_secretsPath);
129+
if (!Directory.Exists(_secretsPath))
128130
{
129-
// the secrets directory contains the host secrets file in addition
130-
// to function secret files
131-
continue;
131+
return;
132132
}
133133

134-
string fileName = Path.GetFileNameWithoutExtension(secretFile.Name);
135-
if (!functionLookup.Contains(fileName))
134+
foreach (var secretFile in secretsDirectory.GetFiles("*.json"))
136135
{
137-
try
136+
if (string.Compare(secretFile.Name, ScriptConstants.HostMetadataFileName, StringComparison.OrdinalIgnoreCase) == 0)
138137
{
139-
secretFile.Delete();
138+
// the secrets directory contains the host secrets file in addition
139+
// to function secret files
140+
continue;
140141
}
141-
catch
142+
143+
string fileName = Path.GetFileNameWithoutExtension(secretFile.Name);
144+
if (!functionLookup.Contains(fileName))
142145
{
143-
// Purge is best effort
146+
try
147+
{
148+
secretFile.Delete();
149+
}
150+
catch
151+
{
152+
// Purge is best effort
153+
}
144154
}
145155
}
146156
}
157+
catch (Exception ex)
158+
{
159+
// Purge is best effort
160+
traceWriter.Error("An error occurred while purging secret files", ex);
161+
}
147162
}
148163

149164
private static string GenerateSecretString()

src/WebJobs.Script.WebHost/WebScriptHostManager.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,7 @@ protected override void OnHostStarted()
174174
}
175175

176176
// Purge any old Function secrets
177-
_secretManager.PurgeOldFiles(Instance.ScriptConfig.RootScriptPath);
177+
_secretManager.PurgeOldFiles(Instance.ScriptConfig.RootScriptPath, Instance.TraceWriter);
178178
}
179179
}
180180
}

src/WebJobs.Script/Host/ScriptHost.cs

Lines changed: 35 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -235,36 +235,49 @@ protected virtual void Initialize()
235235
/// </summary>
236236
private void PurgeOldLogDirectories()
237237
{
238-
if (!Directory.Exists(this.ScriptConfig.RootScriptPath))
238+
try
239239
{
240-
return;
241-
}
240+
if (!Directory.Exists(this.ScriptConfig.RootScriptPath))
241+
{
242+
return;
243+
}
242244

243-
// Create a lookup of all potential functions (whether they're valid or not)
244-
// It is important that we determine functions based on the presence of a folder,
245-
// not whether we've identified a valid function from that folder. This ensures
246-
// that we don't delete logs/secrets for functions that transition into/out of
247-
// invalid unparsable states.
248-
var functionLookup = Directory.EnumerateDirectories(this.ScriptConfig.RootScriptPath).ToLookup(p => Path.GetFileName(p), StringComparer.OrdinalIgnoreCase);
245+
// Create a lookup of all potential functions (whether they're valid or not)
246+
// It is important that we determine functions based on the presence of a folder,
247+
// not whether we've identified a valid function from that folder. This ensures
248+
// that we don't delete logs/secrets for functions that transition into/out of
249+
// invalid unparsable states.
250+
var functionLookup = Directory.EnumerateDirectories(this.ScriptConfig.RootScriptPath).ToLookup(p => Path.GetFileName(p), StringComparer.OrdinalIgnoreCase);
249251

250-
string rootLogFilePath = Path.Combine(this.ScriptConfig.RootLogPath, "Function");
251-
var logFileDirectory = new DirectoryInfo(rootLogFilePath);
252-
foreach (var logDir in logFileDirectory.GetDirectories())
253-
{
254-
if (!functionLookup.Contains(logDir.Name))
252+
string rootLogFilePath = Path.Combine(this.ScriptConfig.RootLogPath, "Function");
253+
if (!Directory.Exists(rootLogFilePath))
255254
{
256-
// the directory no longer maps to a running function
257-
// so delete it
258-
try
259-
{
260-
logDir.Delete(recursive: true);
261-
}
262-
catch
255+
return;
256+
}
257+
258+
var logFileDirectory = new DirectoryInfo(rootLogFilePath);
259+
foreach (var logDir in logFileDirectory.GetDirectories())
260+
{
261+
if (!functionLookup.Contains(logDir.Name))
263262
{
264-
// Purge is best effort
263+
// the directory no longer maps to a running function
264+
// so delete it
265+
try
266+
{
267+
logDir.Delete(recursive: true);
268+
}
269+
catch
270+
{
271+
// Purge is best effort
272+
}
265273
}
266274
}
267275
}
276+
catch (Exception ex)
277+
{
278+
// Purge is best effort
279+
TraceWriter.Error("An error occurred while purging log files", ex);
280+
}
268281
}
269282

270283
public static ScriptHost Create(ScriptHostConfiguration scriptConfig = null)

src/WebJobs.Script/Host/ScriptHostManager.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,8 @@ public void Stop()
212212
{
213213
instance.Dispose();
214214
}
215+
216+
IsRunning = false;
215217
}
216218
catch
217219
{

test/WebJobs.Script.Tests/ScriptHostManagerTests.cs

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,13 @@
33

44
using System;
55
using System.IO;
6+
using System.Linq;
67
using System.Threading;
78
using System.Threading.Tasks;
8-
using Microsoft.Azure.WebJobs.Script;
99
using Microsoft.WindowsAzure.Storage.Blob;
1010
using Moq;
1111
using Moq.Protected;
12+
using Newtonsoft.Json.Linq;
1213
using Xunit;
1314

1415
namespace Microsoft.Azure.WebJobs.Script.Tests
@@ -129,6 +130,45 @@ await TestHelpers.Await(() =>
129130
Assert.Null(target.Object.LastError);
130131
}
131132

133+
[Fact]
134+
public async Task EmptyHost_StartsSuccessfully()
135+
{
136+
string functionDir = Path.Combine(TestHelpers.FunctionsTestDirectory, "Functions", Guid.NewGuid().ToString());
137+
Directory.CreateDirectory(functionDir);
138+
139+
// important for the repro that this directory does not exist
140+
string logDir = Path.Combine(TestHelpers.FunctionsTestDirectory, "Logs", Guid.NewGuid().ToString());
141+
142+
JObject hostConfig = new JObject
143+
{
144+
{ "id", "123456" }
145+
};
146+
File.WriteAllText(Path.Combine(functionDir, ScriptConstants.HostMetadataFileName), hostConfig.ToString());
147+
148+
ScriptHostConfiguration config = new ScriptHostConfiguration
149+
{
150+
RootScriptPath = functionDir,
151+
RootLogPath = logDir,
152+
FileLoggingEnabled = true
153+
};
154+
ScriptHostManager hostManager = new ScriptHostManager(config);
155+
156+
Task runTask = Task.Run(() => hostManager.RunAndBlock());
157+
158+
await TestHelpers.Await(() => hostManager.IsRunning, timeout: 10000);
159+
160+
hostManager.Stop();
161+
Assert.False(hostManager.IsRunning);
162+
163+
string hostLogFilePath = Directory.EnumerateFiles(Path.Combine(logDir, "Host")).Single();
164+
string hostLogs = File.ReadAllText(hostLogFilePath);
165+
166+
Assert.True(hostLogs.Contains("Generating 0 job function(s)"));
167+
Assert.True(hostLogs.Contains("No job functions found."));
168+
Assert.True(hostLogs.Contains("Job host started"));
169+
Assert.True(hostLogs.Contains("Job host stopped"));
170+
}
171+
132172
// Update the manifest for the timer function
133173
// - this will cause a file touch which cause ScriptHostManager to notice and update
134174
// - set to a new output location so that we can ensure we're getting new changes.

test/WebJobs.Script.Tests/TestHelpers.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,18 @@ namespace Microsoft.Azure.WebJobs.Script.Tests
1313
{
1414
public static class TestHelpers
1515
{
16+
/// <summary>
17+
/// Common root directory that functions tests create temporary directories under.
18+
/// This enables us to clean up test files by deleting this single directory.
19+
/// </summary>
20+
public static string FunctionsTestDirectory
21+
{
22+
get
23+
{
24+
return Path.Combine(Path.GetTempPath(), "FunctionsTest");
25+
}
26+
}
27+
1628
public static async Task Await(Func<bool> condition, int timeout = 60 * 1000, int pollingInterval = 2 * 1000)
1729
{
1830
DateTime start = DateTime.Now;

test/WebJobs.Script.Tests/WebScriptHostManagerTests.cs

Lines changed: 67 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
using System.Linq;
88
using System.Threading.Tasks;
99
using Microsoft.Azure.WebJobs.Script.Tests;
10+
using Newtonsoft.Json.Linq;
1011
using Xunit;
1112

1213
namespace Microsoft.Azure.WebJobs.Script.WebHost
@@ -46,19 +47,60 @@ public void SecretFilesArePurgedOnStartup()
4647
Assert.Equal("WebHookTrigger.json", secretFiles[3]);
4748
}
4849

50+
[Fact]
51+
public async Task EmptyHost_StartsSuccessfully()
52+
{
53+
string functionTestDir = Path.Combine(_fixture.TestFunctionRoot, Guid.NewGuid().ToString());
54+
Directory.CreateDirectory(functionTestDir);
55+
56+
// important for the repro that these directories no not exist
57+
string logDir = Path.Combine(_fixture.TestLogsRoot, Guid.NewGuid().ToString());
58+
string secretsDir = Path.Combine(_fixture.TestSecretsRoot, Guid.NewGuid().ToString());
59+
60+
JObject hostConfig = new JObject
61+
{
62+
{ "id", "123456" }
63+
};
64+
File.WriteAllText(Path.Combine(functionTestDir, ScriptConstants.HostMetadataFileName), hostConfig.ToString());
65+
66+
ScriptHostConfiguration config = new ScriptHostConfiguration
67+
{
68+
RootScriptPath = functionTestDir,
69+
RootLogPath = logDir,
70+
FileLoggingEnabled = true
71+
};
72+
SecretManager secretManager = new SecretManager(secretsDir);
73+
ScriptHostManager hostManager = new WebScriptHostManager(config, secretManager);
74+
75+
Task runTask = Task.Run(() => hostManager.RunAndBlock());
76+
77+
await TestHelpers.Await(() => hostManager.IsRunning, timeout: 10000);
78+
79+
hostManager.Stop();
80+
Assert.False(hostManager.IsRunning);
81+
82+
string hostLogFilePath = Directory.EnumerateFiles(Path.Combine(logDir, "Host")).Single();
83+
string hostLogs = File.ReadAllText(hostLogFilePath);
84+
85+
Assert.True(hostLogs.Contains("Generating 0 job function(s)"));
86+
Assert.True(hostLogs.Contains("No job functions found."));
87+
Assert.True(hostLogs.Contains("Job host started"));
88+
Assert.True(hostLogs.Contains("Job host stopped"));
89+
}
90+
4991
public class Fixture : IDisposable
5092
{
5193
public Fixture()
5294
{
53-
string testRoot = Path.Combine(Path.GetTempPath(), "FunctionTests");
54-
if (Directory.Exists(testRoot))
55-
{
56-
Directory.Delete(testRoot, recursive: true);
57-
}
95+
TestFunctionRoot = Path.Combine(TestHelpers.FunctionsTestDirectory, "Functions");
96+
TestLogsRoot = Path.Combine(TestHelpers.FunctionsTestDirectory, "Logs");
97+
TestSecretsRoot = Path.Combine(TestHelpers.FunctionsTestDirectory, "Secrets");
5898

59-
SecretsPath = Path.Combine(testRoot, "TestSecrets");
99+
string testRoot = Path.Combine(TestFunctionRoot, Guid.NewGuid().ToString());
100+
101+
SecretsPath = Path.Combine(TestSecretsRoot, Guid.NewGuid().ToString());
60102
Directory.CreateDirectory(SecretsPath);
61-
string logRoot = Path.Combine(testRoot, @"Functions");
103+
string logRoot = Path.Combine(TestLogsRoot, Guid.NewGuid().ToString(), @"Functions");
62104
Directory.CreateDirectory(logRoot);
63105
FunctionsLogDir = Path.Combine(logRoot, @"Function");
64106
Directory.CreateDirectory(FunctionsLogDir);
@@ -100,13 +142,31 @@ public Fixture()
100142

101143
public string SecretsPath { get; private set; }
102144

145+
public string TestFunctionRoot { get; private set; }
146+
147+
public string TestLogsRoot { get; private set; }
148+
149+
public string TestSecretsRoot { get; private set; }
150+
103151
public void Dispose()
104152
{
105153
if (HostManager != null)
106154
{
107155
HostManager.Stop();
108156
HostManager.Dispose();
109157
}
158+
159+
try
160+
{
161+
if (Directory.Exists(TestHelpers.FunctionsTestDirectory))
162+
{
163+
Directory.Delete(TestHelpers.FunctionsTestDirectory, recursive: true);
164+
}
165+
}
166+
catch
167+
{
168+
// occasionally get file in use errors
169+
}
110170
}
111171

112172
private void CreateTestFunctionLogs(string logRoot, string functionName)

0 commit comments

Comments
 (0)