Skip to content

Commit 7fc5d53

Browse files
MikeStallmathewc
authored andcommitted
When a file is changed, wee currently wait for all outstanding functions to exit and then pick up the new changes.
Change this to orphan the existing ScriptHost (stop listening, but finish current functions), and immediately spin up a new host.
1 parent 229d8be commit 7fc5d53

File tree

6 files changed

+202
-20
lines changed

6 files changed

+202
-20
lines changed

src/WebJobs.Script/Host/ScriptHostManager.cs

Lines changed: 67 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@
22
// Licensed under the MIT License. See License.txt in the project root for license information.
33

44
using System;
5+
using System.Collections.Generic;
6+
using System.Linq;
57
using System.Threading;
8+
using System.Threading.Tasks;
69

710
namespace Microsoft.Azure.WebJobs.Script
811
{
@@ -13,8 +16,16 @@ namespace Microsoft.Azure.WebJobs.Script
1316
public class ScriptHostManager : IDisposable
1417
{
1518
private readonly ScriptHostConfiguration _config;
16-
private ScriptHost _instance;
19+
private ScriptHost _currentInstance;
20+
21+
// ScriptHosts are not thread safe, so be clear that only 1 thread at a time operates on each instance.
22+
// List of all outstanding ScriptHost instances. Only 1 of these (specified by _currentInstance)
23+
// should be listening at a time. The others are "orphaned" and exist to finish executing any functions
24+
// and will then remove themselves from this list.
25+
private HashSet<ScriptHost> _liveInstances = new HashSet<ScriptHost>();
26+
1727
private bool _stopped;
28+
private AutoResetEvent _stopEvent = new AutoResetEvent(false);
1829

1930
public ScriptHostManager(ScriptHostConfiguration config)
2031
{
@@ -31,7 +42,7 @@ public ScriptHost Instance
3142
{
3243
get
3344
{
34-
return _instance;
45+
return _currentInstance;
3546
}
3647
}
3748

@@ -47,7 +58,11 @@ public ScriptHost Instance
4758
ScriptHost newInstance = ScriptHost.Create(_config);
4859

4960
newInstance.Start();
50-
_instance = newInstance;
61+
lock (_liveInstances)
62+
{
63+
_liveInstances.Add(newInstance);
64+
}
65+
_currentInstance = newInstance;
5166
OnHostStarted();
5267

5368
// only after ALL initialization is complete do we set this flag
@@ -58,31 +73,69 @@ public ScriptHost Instance
5873
// signaled. That is fine - the restart will be processed immediately
5974
// once we get to this line again. The important thing is that these
6075
// restarts are only happening on a single thread.
61-
_instance.RestartEvent.WaitOne();
76+
WaitHandle.WaitAny(new WaitHandle[] {
77+
newInstance.RestartEvent,
78+
_stopEvent
79+
});
6280

63-
// stop the host fully
64-
_instance.Stop();
65-
_instance.Dispose();
81+
// Orphan the current host instance. We're stopping it, so it won't listen for any new functions
82+
// it will finish any currently executing functions and then clean itself up.
83+
// Spin around and create a new host instance.
84+
Task tIgnore = Orphan(newInstance);
6685
}
6786
while (!_stopped);
6887
}
6988

89+
// Let the existing host instance finsih currently executing functions.
90+
private async Task Orphan(ScriptHost instance)
91+
{
92+
lock (_liveInstances)
93+
{
94+
bool removed = _liveInstances.Remove(instance);
95+
if (!removed)
96+
{
97+
return; // somebody else is handling it
98+
}
99+
}
100+
101+
// this thread now owns the instance
102+
await instance.StopAsync();
103+
instance.Dispose();
104+
}
105+
70106
public void Stop()
71107
{
72108
_stopped = true;
73109

74110
try
75111
{
76-
if (_instance != null)
112+
_stopEvent.Set();
113+
ScriptHost[] instances = GetLiveInstancesAndClear();
114+
115+
Task[] tasksStop = Array.ConvertAll(instances, instance => instance.StopAsync());
116+
Task.WaitAll(tasksStop);
117+
118+
foreach (var instance in instances)
77119
{
78-
_instance.Stop();
79-
_instance.Dispose();
120+
instance.Dispose();
80121
}
81122
}
82123
catch
83124
{
84125
// best effort
85-
}
126+
}
127+
}
128+
129+
private ScriptHost[] GetLiveInstancesAndClear()
130+
{
131+
ScriptHost[] instances;
132+
lock (_liveInstances)
133+
{
134+
instances = _liveInstances.ToArray();
135+
_liveInstances.Clear();
136+
}
137+
138+
return instances;
86139
}
87140

88141
protected virtual void OnHostStarted()
@@ -91,9 +144,10 @@ protected virtual void OnHostStarted()
91144

92145
public void Dispose()
93146
{
94-
if (_instance != null)
147+
ScriptHost[] instances = GetLiveInstancesAndClear();
148+
foreach (var instance in instances)
95149
{
96-
_instance.Dispose();
150+
instance.Dispose();
97151
}
98152
}
99153
}

test/WebJobs.Script.Tests/NodeEndToEndTests.cs

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
using System.Threading.Tasks;
1111
using Newtonsoft.Json.Linq;
1212
using Xunit;
13+
using System.Diagnostics;
1314

1415
namespace WebJobs.Script.Tests
1516
{
@@ -143,12 +144,32 @@ public async Task WebHookTrigger_GenericJson()
143144
[Fact]
144145
public async Task TimerTrigger()
145146
{
146-
// job is running every second, so give it a few seconds to
147-
// generate some output
148-
await Task.Delay(4000);
147+
var stopwatch = Stopwatch.StartNew();
148+
149+
while (true)
150+
{
151+
// Time will write to this file.
152+
try
153+
{
154+
string[] lines = File.ReadAllLines("joblog.txt");
155+
if (lines.Length > 2)
156+
{
157+
return;
158+
}
159+
}
160+
catch
161+
{
162+
// File may be missing if timer hasn't written yet.
163+
}
164+
165+
if (stopwatch.ElapsedMilliseconds > 6*1000)
166+
{
167+
Assert.True(false, "Timeout waiting for timer to fire.");
168+
}
169+
170+
await Task.Delay(TimeSpan.FromSeconds(1));
171+
}
149172

150-
string[] lines = File.ReadAllLines("joblog.txt");
151-
Assert.True(lines.Length > 2);
152173
}
153174

154175
public class TestFixture : EndToEndTestFixture
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
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.Collections.Generic;
6+
using System.IO;
7+
using System.Net;
8+
using System.Net.Http;
9+
using System.Net.Http.Headers;
10+
using System.Threading.Tasks;
11+
using Newtonsoft.Json.Linq;
12+
using Xunit;
13+
using System.Diagnostics;
14+
using Microsoft.Azure.WebJobs.Script;
15+
using System.Threading;
16+
using Microsoft.WindowsAzure.Storage.Blob;
17+
18+
namespace WebJobs.Script.Tests
19+
{
20+
[Trait("Category", "E2E")]
21+
public class ScriptHostManagerTests
22+
{
23+
// Update a script file (the function.json) to force the ScriptHost to re-index and pick up new changes.
24+
// Test with timers:
25+
[Fact]
26+
public async Task UpdateFileAndRestart()
27+
{
28+
Random r = new Random();
29+
30+
CancellationTokenSource cts = new CancellationTokenSource();
31+
32+
var fixture = new NodeEndToEndTests.TestFixture();
33+
var blob1 = UpdateOutputName("outputblobname", "first", fixture);
34+
35+
await fixture.Host.StopAsync();
36+
var config = fixture.Host.ScriptConfig;
37+
38+
using (var manager = new ScriptHostManager(config))
39+
{
40+
// Background task to run while the main thread is pumping events at RunAndBlock().
41+
Thread t = new Thread(_ =>
42+
{
43+
// Wait for initial execution.
44+
Wait(blob1);
45+
46+
// This changes the bindings so that we now write to blob2
47+
var blob2 = UpdateOutputName("first", "second", fixture);
48+
49+
// wait for newly executed
50+
Wait(blob2);
51+
52+
manager.Stop();
53+
});
54+
t.Start();
55+
56+
manager.RunAndBlock(cts.Token);
57+
58+
t.Join();
59+
}
60+
}
61+
62+
// For a blob to appear. This is evidence that the function ran.
63+
void Wait(CloudBlockBlob blob)
64+
{
65+
Stopwatch sw = Stopwatch.StartNew();
66+
const int timeoutMs = 10 * 1000; //
67+
68+
while (!blob.Exists())
69+
{
70+
Thread.Sleep(500);
71+
if (sw.ElapsedMilliseconds > timeoutMs)
72+
{
73+
// If no blob appeared yet, then the function didn't run.
74+
// It may have not picked up the new changes
75+
Assert.True(false, "Timeout waiting for blob to appear. " + timeoutMs + "ms");
76+
}
77+
}
78+
}
79+
80+
// Update the manifest for the timer function
81+
// - this will cause a file touch which cause ScriptHostManager to notice and update
82+
// - set to a new output location so that we can ensure we're getting new changes.
83+
static CloudBlockBlob UpdateOutputName(string prev, string hint, EndToEndTestFixture fixture)
84+
{
85+
string name = hint;
86+
87+
string manifestPath = Path.Combine(Environment.CurrentDirectory, @"TestScripts\Node\TimerTrigger\function.json");
88+
string content = File.ReadAllText(manifestPath);
89+
content = content.Replace(prev, name);
90+
File.WriteAllText(manifestPath, content);
91+
92+
var blob = fixture.TestContainer.GetBlockBlobReference(name);
93+
blob.DeleteIfExists();
94+
return blob;
95+
96+
}
97+
}
98+
}

test/WebJobs.Script.Tests/TestScripts/Node/TimerTrigger/function.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,13 @@
55
"type": "timerTrigger",
66
"schedule": "* * * * * *"
77
}
8+
],
9+
"output": [
10+
{
11+
"type": "blob",
12+
"name": "output",
13+
"path": "test-output/outputblobname"
14+
}
815
]
916
}
1017
}
Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
var fs = require('fs');
22

33
module.exports = function (context) {
4-
var timeStamp = new Date().toISOString();
4+
var timeStamp = new Date().toISOString();
55
fs.appendFile('joblog.txt', timeStamp + '\r\n', function (err) {
6-
context.done(err);
6+
var blobOutputContents = "from timer trigger: " + timeStamp;
7+
context.done(err, blobOutputContents);
78
});
89
}

test/WebJobs.Script.Tests/WebJobs.Script.Tests.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,7 @@
178178
<Compile Include="PhpEndToEndTests.cs" />
179179
<Compile Include="PowershellEndToEndTests.cs" />
180180
<Compile Include="PythonEndToEndTests.cs" />
181+
<Compile Include="ScriptHostManagerTests.cs" />
181182
<Compile Include="ScriptHostTests.cs" />
182183
<Compile Include="NodeEndToEndTests.cs" />
183184
<Compile Include="NodeFunctionGenerationTests.cs" />

0 commit comments

Comments
 (0)