Skip to content

Commit 757fa4f

Browse files
committed
Ensuring that the ScriptHost is Disposed of in case of an exception in the run loop.
1 parent 0f2299a commit 757fa4f

File tree

5 files changed

+101
-7
lines changed

5 files changed

+101
-7
lines changed

src/WebJobs.Script/Host/ScriptHostManager.cs

Lines changed: 41 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ namespace Microsoft.Azure.WebJobs.Script
1919
public class ScriptHostManager : IDisposable
2020
{
2121
private readonly ScriptHostConfiguration _config;
22+
private readonly IScriptHostFactory _scriptHostFactory;
2223
private ScriptHost _currentInstance;
2324

2425
// ScriptHosts are not thread safe, so be clear that only 1 thread at a time operates on each instance.
@@ -33,8 +34,14 @@ public class ScriptHostManager : IDisposable
3334
private TraceWriter _traceWriter;
3435

3536
public ScriptHostManager(ScriptHostConfiguration config)
37+
: this(config, new ScriptHostFactory())
38+
{
39+
}
40+
41+
public ScriptHostManager(ScriptHostConfiguration config, IScriptHostFactory scriptHostFactory)
3642
{
3743
_config = config;
44+
_scriptHostFactory = scriptHostFactory;
3845
}
3946

4047
/// <summary>
@@ -57,6 +64,7 @@ public ScriptHost Instance
5764
// host level configuration files change
5865
do
5966
{
67+
ScriptHost newInstance = null;
6068
try
6169
{
6270
IsRunning = false;
@@ -66,7 +74,7 @@ public ScriptHost Instance
6674
{
6775
HostId = _config.HostConfig.HostId
6876
};
69-
ScriptHost newInstance = ScriptHost.Create(_config);
77+
newInstance = _scriptHostFactory.Create(_config);
7078
_traceWriter = newInstance.TraceWriter;
7179

7280
// write any function initialization errors to the log file
@@ -108,6 +116,20 @@ public ScriptHost Instance
108116
_traceWriter.Error("A ScriptHost error occurred", ex);
109117
}
110118

119+
// If a ScriptHost instance was created before the exception was thrown
120+
// Orphan and cleanup that instance.
121+
if (newInstance != null)
122+
{
123+
Orphan(newInstance, forceStop: true)
124+
.ContinueWith(t =>
125+
{
126+
if (t.IsFaulted)
127+
{
128+
t.Exception.Handle(e => true);
129+
}
130+
});
131+
}
132+
111133
// Wait for a short period of time before restarting to
112134
// avoid cases where a host level config error might cause
113135
// a rapid restart cycle
@@ -132,21 +154,33 @@ private static void LogErrors(ScriptHost host)
132154
}
133155
}
134156

135-
// Let the existing host instance finish currently executing functions.
136-
private async Task Orphan(ScriptHost instance)
157+
/// <summary>
158+
/// Remove the <see cref="ScriptHost"/> instance from the live instances collection,
159+
/// allowing it to finish currently executing functions before stopping and disposing of it.
160+
/// </summary>
161+
/// <param name="instance">The <see cref="ScriptHost"/> instance to remove</param>
162+
/// <param name="forceStop">Forces the call to stop and dispose of the instance, even if it isn't present in the live instances collection.</param>
163+
/// <returns></returns>
164+
private async Task Orphan(ScriptHost instance, bool forceStop = false)
137165
{
138166
lock (_liveInstances)
139167
{
140168
bool removed = _liveInstances.Remove(instance);
141-
if (!removed)
169+
if (!forceStop && !removed)
142170
{
143171
return; // somebody else is handling it
144172
}
145173
}
146174

147-
// this thread now owns the instance
148-
await instance.StopAsync();
149-
instance.Dispose();
175+
try
176+
{
177+
// this thread now owns the instance
178+
await instance.StopAsync();
179+
}
180+
finally
181+
{
182+
instance.Dispose();
183+
}
150184
}
151185

152186
public void Stop()
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+
public interface IScriptHostFactory
7+
{
8+
ScriptHost Create(ScriptHostConfiguration config);
9+
}
10+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
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+
public sealed class ScriptHostFactory : IScriptHostFactory
7+
{
8+
public ScriptHost Create(ScriptHostConfiguration config)
9+
{
10+
return ScriptHost.Create(config);
11+
}
12+
}
13+
}

src/WebJobs.Script/WebJobs.Script.csproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,10 +254,12 @@
254254
<Compile Include="Extensions\ActionExtensions.cs" />
255255
<Compile Include="FileTraceWriter.cs" />
256256
<Compile Include="HttpMethodJsonConverter.cs" />
257+
<Compile Include="IScriptHostFactory.cs" />
257258
<Compile Include="NullTraceWriter.cs" />
258259
<Compile Include="Properties\AssemblyInfo.cs" />
259260
<Compile Include="Config\ScriptHostConfiguration.cs" />
260261
<Compile Include="Config\TypeLocator.cs" />
262+
<Compile Include="ScriptHostFactory.cs" />
261263
</ItemGroup>
262264
<ItemGroup>
263265
<None Include="app.config">

test/WebJobs.Script.Tests/ScriptHostManagerTests.cs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
using System.Threading.Tasks;
88
using Microsoft.Azure.WebJobs.Script;
99
using Microsoft.WindowsAzure.Storage.Blob;
10+
using Moq;
11+
using Moq.Protected;
1012
using Xunit;
1113

1214
namespace WebJobs.Script.Tests
@@ -59,6 +61,32 @@ public async Task UpdateFileAndRestart()
5961
}
6062
}
6163

64+
[Fact]
65+
public void RunAndBlock_DisposesOfHost_WhenExceptionIsThrown()
66+
{
67+
ScriptHostConfiguration config = new ScriptHostConfiguration()
68+
{
69+
RootScriptPath = Environment.CurrentDirectory,
70+
TraceWriter = NullTraceWriter.Instance
71+
};
72+
73+
var hostMock = new Mock<TestScriptHost>(config);
74+
var factoryMock = new Mock<IScriptHostFactory>();
75+
factoryMock.Setup(f => f.Create(It.IsAny<ScriptHostConfiguration>()))
76+
.Returns(hostMock.Object);
77+
78+
var target = new Mock<ScriptHostManager>(config, factoryMock.Object);
79+
target.Protected().Setup("OnHostStarted")
80+
.Throws(new Exception());
81+
82+
hostMock.Protected().Setup("Dispose", true)
83+
.Callback(() => target.Object.Stop());
84+
85+
Task.Run(() => target.Object.RunAndBlock()).Wait(5000);
86+
87+
hostMock.Protected().Verify("Dispose", Times.Once(), true);
88+
}
89+
6290
// Update the manifest for the timer function
6391
// - this will cause a file touch which cause ScriptHostManager to notice and update
6492
// - set to a new output location so that we can ensure we're getting new changes.
@@ -75,5 +103,12 @@ private static CloudBlockBlob UpdateOutputName(string prev, string hint, EndToEn
75103
blob.DeleteIfExists();
76104
return blob;
77105
}
106+
107+
public class TestScriptHost : ScriptHost
108+
{
109+
public TestScriptHost(ScriptHostConfiguration scriptConfig) : base(scriptConfig)
110+
{
111+
}
112+
}
78113
}
79114
}

0 commit comments

Comments
 (0)