Skip to content

Commit 7b702c6

Browse files
committed
Adding internal filewatch logic to efficiently reload functions when changed
1 parent e0a783e commit 7b702c6

19 files changed

+260
-99
lines changed

src/WebJobs.Script.Host/Program.cs

+8-2
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,14 @@ static void Main(string[] args)
2222
RootPath = rootPath
2323
};
2424

25-
ScriptHost host = ScriptHost.Create(config);
26-
host.RunAndBlock();
25+
// Start the host and restart it if requested
26+
ScriptHost host = null;
27+
do
28+
{
29+
host = ScriptHost.Create(config);
30+
host.RunAndBlock();
31+
}
32+
while (host.Restart);
2733
}
2834
}
2935
}

src/WebJobs.Script/Binding/Binding.cs

+5-5
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,9 @@ namespace Microsoft.Azure.WebJobs.Script
1414
{
1515
internal abstract class Binding
1616
{
17-
private readonly JobHostConfiguration _config;
17+
private readonly ScriptHostConfiguration _config;
1818

19-
protected Binding(JobHostConfiguration config, string name, string type, FileAccess fileAccess, bool isTrigger)
19+
protected Binding(ScriptHostConfiguration config, string name, string type, FileAccess fileAccess, bool isTrigger)
2020
{
2121
_config = config;
2222
Name = name;
@@ -37,7 +37,7 @@ protected Binding(JobHostConfiguration config, string name, string type, FileAcc
3737

3838
public abstract Task BindAsync(IBinder binder, Stream stream, IReadOnlyDictionary<string, string> bindingData);
3939

40-
internal static Collection<Binding> GetBindings(JobHostConfiguration config, JArray bindingArray, FileAccess fileAccess)
40+
internal static Collection<Binding> GetBindings(ScriptHostConfiguration config, JArray bindingArray, FileAccess fileAccess)
4141
{
4242
Collection<Binding> bindings = new Collection<Binding>();
4343

@@ -103,12 +103,12 @@ internal static Collection<Binding> GetBindings(JobHostConfiguration config, JAr
103103

104104
protected string Resolve(string name)
105105
{
106-
if (_config.NameResolver == null)
106+
if (_config.HostConfig.NameResolver == null)
107107
{
108108
return name;
109109
}
110110

111-
return _config.NameResolver.ResolveWholeString(name);
111+
return _config.HostConfig.NameResolver.ResolveWholeString(name);
112112
}
113113
}
114114
}

src/WebJobs.Script/Binding/BlobBinding.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ internal class BlobBinding : Binding
1313
{
1414
private readonly BindingTemplate _pathBindingTemplate;
1515

16-
public BlobBinding(JobHostConfiguration config, string name, string path, FileAccess fileAccess, bool isTrigger) : base(config, name, "blob", fileAccess, isTrigger)
16+
public BlobBinding(ScriptHostConfiguration config, string name, string path, FileAccess fileAccess, bool isTrigger) : base(config, name, "blob", fileAccess, isTrigger)
1717
{
1818
Path = path;
1919
_pathBindingTemplate = BindingTemplate.FromString(Path);

src/WebJobs.Script/Binding/QueueBinding.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ internal class QueueBinding : Binding
1313
{
1414
private readonly BindingTemplate _queueNameBindingTemplate;
1515

16-
public QueueBinding(JobHostConfiguration config, string name, string queueName, FileAccess fileAccess, bool isTrigger) : base(config, name, "queue", fileAccess, isTrigger)
16+
public QueueBinding(ScriptHostConfiguration config, string name, string queueName, FileAccess fileAccess, bool isTrigger) : base(config, name, "queue", fileAccess, isTrigger)
1717
{
1818
QueueName = queueName;
1919
_queueNameBindingTemplate = BindingTemplate.FromString(QueueName);

src/WebJobs.Script/Binding/ServiceBusBinding.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ internal class ServiceBusBinding : Binding
1313
{
1414
private readonly BindingTemplate _queueOrTopicNameBindingTemplate;
1515

16-
public ServiceBusBinding(JobHostConfiguration config, string name, string queueOrTopicName, FileAccess fileAccess, bool isTrigger) : base(config, name, "serviceBus", fileAccess, isTrigger)
16+
public ServiceBusBinding(ScriptHostConfiguration config, string name, string queueOrTopicName, FileAccess fileAccess, bool isTrigger) : base(config, name, "serviceBus", fileAccess, isTrigger)
1717
{
1818
QueueOrTopicName = queueOrTopicName;
1919
_queueOrTopicNameBindingTemplate = BindingTemplate.FromString(QueueOrTopicName);

src/WebJobs.Script/Binding/TableBinding.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ internal class TableBinding : Binding
1919
private readonly BindingTemplate _rowKeyBindingTemplate;
2020
private readonly TableQuery _tableQuery;
2121

22-
public TableBinding(JobHostConfiguration config, string name, string tableName, string partitionKey, string rowKey, FileAccess fileAccess, TableQuery tableQuery = null) : base(config, name, "queue", fileAccess, false)
22+
public TableBinding(ScriptHostConfiguration config, string name, string tableName, string partitionKey, string rowKey, FileAccess fileAccess, TableQuery tableQuery = null) : base(config, name, "queue", fileAccess, false)
2323
{
2424
TableName = tableName;
2525
PartitionKey = partitionKey;

src/WebJobs.Script/Config/ScriptHost.cs

+85-29
Original file line numberDiff line numberDiff line change
@@ -16,37 +16,37 @@ namespace Microsoft.Azure.WebJobs.Script
1616
public class ScriptHost : JobHost
1717
{
1818
private const string HostAssemblyName = "ScriptHost";
19+
private FileSystemWatcher _fileWatcher;
1920

20-
protected ScriptHost(JobHostConfiguration config, ScriptHostConfiguration scriptConfig)
21-
: base(config)
21+
protected ScriptHost(ScriptHostConfiguration scriptConfig)
22+
: base(scriptConfig.HostConfig)
2223
{
23-
HostConfig = config;
2424
ScriptConfig = scriptConfig;
2525
}
2626

27-
public JobHostConfiguration HostConfig { get; private set; }
28-
2927
public ScriptHostConfiguration ScriptConfig { get; private set; }
3028

29+
public bool Restart { get; private set; }
30+
3131
protected virtual void Initialize()
3232
{
3333
List<FunctionDescriptorProvider> descriptionProviders = new List<FunctionDescriptorProvider>()
3434
{
35-
new ScriptFunctionDescriptorProvider(HostConfig, ScriptConfig.RootPath),
36-
new NodeFunctionDescriptorProvider(HostConfig, ScriptConfig.RootPath)
35+
new ScriptFunctionDescriptorProvider(ScriptConfig),
36+
new NodeFunctionDescriptorProvider(this, ScriptConfig)
3737
};
3838

39-
if (HostConfig.IsDevelopment)
39+
if (ScriptConfig.HostConfig.IsDevelopment)
4040
{
41-
HostConfig.UseDevelopmentSettings();
41+
ScriptConfig.HostConfig.UseDevelopmentSettings();
4242
}
4343

4444
// read host.json and apply to JobHostConfiguration
4545
string hostConfigFilePath = Path.Combine(ScriptConfig.RootPath, "host.json");
4646
Console.WriteLine(string.Format("Reading host configuration file '{0}'", hostConfigFilePath));
4747
string json = File.ReadAllText(hostConfigFilePath);
4848
JObject hostConfig = JObject.Parse(json);
49-
ApplyConfiguration(hostConfig, HostConfig);
49+
ApplyConfiguration(hostConfig, ScriptConfig);
5050

5151
// read all script functions and apply to JobHostConfiguration
5252
Collection<FunctionDescriptor> functions = ReadFunctions(ScriptConfig, descriptionProviders);
@@ -57,8 +57,33 @@ protected virtual void Initialize()
5757
List<Type> types = new List<Type>();
5858
types.Add(type);
5959

60-
HostConfig.TypeLocator = new TypeLocator(types);
61-
HostConfig.NameResolver = new NameResolver();
60+
ScriptConfig.HostConfig.TypeLocator = new TypeLocator(types);
61+
ScriptConfig.HostConfig.NameResolver = new NameResolver();
62+
63+
if (ScriptConfig.WatchFiles)
64+
{
65+
_fileWatcher = new FileSystemWatcher(ScriptConfig.RootPath, "*.json")
66+
{
67+
IncludeSubdirectories = true,
68+
EnableRaisingEvents = true
69+
};
70+
_fileWatcher.Changed += OnConfigurationFileChanged;
71+
}
72+
}
73+
74+
private void StopAndRestart()
75+
{
76+
if (Restart)
77+
{
78+
// we've already received a restart call
79+
return;
80+
}
81+
82+
Console.WriteLine("The host configuration file has changed. Restarting.");
83+
84+
// Flag for restart and stop the host.
85+
Restart = true;
86+
Stop();
6287
}
6388

6489
public static ScriptHost Create(ScriptHostConfiguration scriptConfig = null)
@@ -76,8 +101,7 @@ public static ScriptHost Create(ScriptHostConfiguration scriptConfig = null)
76101
scriptConfig.RootPath = Path.Combine(Environment.CurrentDirectory, scriptConfig.RootPath);
77102
}
78103

79-
JobHostConfiguration config = new JobHostConfiguration();
80-
ScriptHost scriptHost = new ScriptHost(config, scriptConfig);
104+
ScriptHost scriptHost = new ScriptHost(scriptConfig);
81105
scriptHost.Initialize();
82106

83107
return scriptHost;
@@ -180,14 +204,22 @@ internal static Collection<FunctionDescriptor> ReadFunctions(List<FunctionFolder
180204
return functionDescriptors;
181205
}
182206

183-
internal static void ApplyConfiguration(JObject config, JobHostConfiguration jobHostConfig)
207+
internal static void ApplyConfiguration(JObject config, ScriptHostConfiguration scriptConfig)
184208
{
209+
JobHostConfiguration hostConfig = scriptConfig.HostConfig;
210+
185211
JToken hostId = (JToken)config["id"];
186212
if (hostId == null)
187213
{
188214
throw new InvalidOperationException("An 'id' must be specified in the host configuration.");
189215
}
190-
jobHostConfig.HostId = (string)hostId;
216+
hostConfig.HostId = (string)hostId;
217+
218+
JToken watchFiles = (JToken)config["watchFiles"];
219+
if (watchFiles != null && watchFiles.Type == JTokenType.Boolean)
220+
{
221+
scriptConfig.WatchFiles = (bool)watchFiles;
222+
}
191223

192224
// Apply Queues configuration
193225
JObject configSection = (JObject)config["queues"];
@@ -196,19 +228,19 @@ internal static void ApplyConfiguration(JObject config, JobHostConfiguration job
196228
{
197229
if (configSection.TryGetValue("maxPollingInterval", out value))
198230
{
199-
jobHostConfig.Queues.MaxPollingInterval = TimeSpan.FromMilliseconds((int)value);
231+
hostConfig.Queues.MaxPollingInterval = TimeSpan.FromMilliseconds((int)value);
200232
}
201233
if (configSection.TryGetValue("batchSize", out value))
202234
{
203-
jobHostConfig.Queues.BatchSize = (int)value;
235+
hostConfig.Queues.BatchSize = (int)value;
204236
}
205237
if (configSection.TryGetValue("maxDequeueCount", out value))
206238
{
207-
jobHostConfig.Queues.MaxDequeueCount = (int)value;
239+
hostConfig.Queues.MaxDequeueCount = (int)value;
208240
}
209241
if (configSection.TryGetValue("newBatchThreshold", out value))
210242
{
211-
jobHostConfig.Queues.NewBatchThreshold = (int)value;
243+
hostConfig.Queues.NewBatchThreshold = (int)value;
212244
}
213245
}
214246

@@ -219,23 +251,23 @@ internal static void ApplyConfiguration(JObject config, JobHostConfiguration job
219251
{
220252
if (configSection.TryGetValue("lockPeriod", out value))
221253
{
222-
jobHostConfig.Singleton.LockPeriod = TimeSpan.Parse((string)value);
254+
hostConfig.Singleton.LockPeriod = TimeSpan.Parse((string)value);
223255
}
224256
if (configSection.TryGetValue("listenerLockPeriod", out value))
225257
{
226-
jobHostConfig.Singleton.ListenerLockPeriod = TimeSpan.Parse((string)value);
258+
hostConfig.Singleton.ListenerLockPeriod = TimeSpan.Parse((string)value);
227259
}
228260
if (configSection.TryGetValue("listenerLockRecoveryPollingInterval", out value))
229261
{
230-
jobHostConfig.Singleton.ListenerLockRecoveryPollingInterval = TimeSpan.Parse((string)value);
262+
hostConfig.Singleton.ListenerLockRecoveryPollingInterval = TimeSpan.Parse((string)value);
231263
}
232264
if (configSection.TryGetValue("lockAcquisitionTimeout", out value))
233265
{
234-
jobHostConfig.Singleton.LockAcquisitionTimeout = TimeSpan.Parse((string)value);
266+
hostConfig.Singleton.LockAcquisitionTimeout = TimeSpan.Parse((string)value);
235267
}
236268
if (configSection.TryGetValue("lockAcquisitionPollingInterval", out value))
237269
{
238-
jobHostConfig.Singleton.LockAcquisitionPollingInterval = TimeSpan.Parse((string)value);
270+
hostConfig.Singleton.LockAcquisitionPollingInterval = TimeSpan.Parse((string)value);
239271
}
240272
}
241273

@@ -250,7 +282,7 @@ internal static void ApplyConfiguration(JObject config, JobHostConfiguration job
250282
sbConfig.MessageOptions.MaxConcurrentCalls = (int)value;
251283
}
252284
}
253-
jobHostConfig.UseServiceBus(sbConfig);
285+
hostConfig.UseServiceBus(sbConfig);
254286

255287
// Apply Tracing configuration
256288
configSection = (JObject)config["tracing"];
@@ -259,7 +291,7 @@ internal static void ApplyConfiguration(JObject config, JobHostConfiguration job
259291
TraceLevel consoleLevel;
260292
if (Enum.TryParse<TraceLevel>((string)value, true, out consoleLevel))
261293
{
262-
jobHostConfig.Tracing.ConsoleLevel = consoleLevel;
294+
hostConfig.Tracing.ConsoleLevel = consoleLevel;
263295
}
264296
}
265297

@@ -270,9 +302,33 @@ internal static void ApplyConfiguration(JObject config, JobHostConfiguration job
270302
{
271303
webHooksConfig = new WebHooksConfiguration((int)value);
272304
}
273-
jobHostConfig.UseWebHooks(webHooksConfig);
305+
hostConfig.UseWebHooks(webHooksConfig);
306+
307+
hostConfig.UseTimers();
308+
}
309+
310+
private void OnConfigurationFileChanged(object sender, FileSystemEventArgs e)
311+
{
312+
string fileName = Path.GetFileName(e.Name);
313+
314+
if (!Restart &&
315+
((string.Compare(fileName, "host.json") == 0) || string.Compare(fileName, "function.json") == 0))
316+
{
317+
StopAndRestart();
318+
}
319+
}
320+
321+
protected override void Dispose(bool disposing)
322+
{
323+
if (disposing)
324+
{
325+
if (_fileWatcher != null)
326+
{
327+
_fileWatcher.Dispose();
328+
}
329+
}
274330

275-
jobHostConfig.UseTimers();
331+
base.Dispose(disposing);
276332
}
277333
}
278334
}
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,34 @@
11
// Copyright (c) .NET Foundation. All rights reserved.
22
// Licensed under the MIT License. See License.txt in the project root for license information.
33

4+
using System;
5+
46
namespace Microsoft.Azure.WebJobs.Script
57
{
68
public class ScriptHostConfiguration
79
{
10+
public ScriptHostConfiguration()
11+
{
12+
HostConfig = new JobHostConfiguration();
13+
WatchFiles = true;
14+
}
15+
16+
/// <summary>
17+
/// Gets the <see cref="JobHostConfiguration"/>.
18+
/// </summary>
19+
public JobHostConfiguration HostConfig { get; private set; }
20+
821
/// <summary>
922
/// Gets or sets the path to the script function directory.
1023
/// </summary>
1124
public string RootPath { get; set; }
25+
26+
/// <summary>
27+
/// Gets or sets a value dicating whether the <see cref="ScriptHost"/> should
28+
/// monitor file for changes (default is true). When set to true, the host will
29+
/// automatically react to source/config file changes. When set to false no file
30+
/// monitoring will be performed.
31+
/// </summary>
32+
public bool WatchFiles { get; set; }
1233
}
1334
}

src/WebJobs.Script/Description/FunctionDescriptorProvider.cs

+15-11
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,24 @@ public abstract class FunctionDescriptorProvider
1414
{
1515
public abstract bool TryCreate(FunctionFolderInfo functionFolderInfo, out FunctionDescriptor functionDescriptor);
1616

17-
protected bool IsDisabled(string functionName, JObject trigger)
17+
protected bool IsDisabled(string functionName, JObject value)
18+
{
19+
if (value != null && IsDisabled(value["disabled"]))
20+
{
21+
Console.WriteLine(string.Format("Function '{0}' is disabled", functionName));
22+
return true;
23+
}
24+
25+
return false;
26+
}
27+
28+
private bool IsDisabled(JToken isDisabledValue)
1829
{
19-
bool isDisabled = false;
20-
JToken isDisabledValue = trigger["disabled"];
2130
if (isDisabledValue != null)
2231
{
2332
if (isDisabledValue.Type == JTokenType.Boolean && (bool)isDisabledValue)
2433
{
25-
isDisabled = true;
34+
return true;
2635
}
2736
else
2837
{
@@ -32,17 +41,12 @@ protected bool IsDisabled(string functionName, JObject trigger)
3241
(string.Compare(value, "1", StringComparison.OrdinalIgnoreCase) == 0 ||
3342
string.Compare(value, "true", StringComparison.OrdinalIgnoreCase) == 0))
3443
{
35-
isDisabled = true;
44+
return true;
3645
}
3746
}
3847
}
3948

40-
if (isDisabled)
41-
{
42-
Console.WriteLine(string.Format("Function '{0}' is disabled", functionName));
43-
}
44-
45-
return isDisabled;
49+
return false;
4650
}
4751

4852
protected ParameterDescriptor ParseQueueTrigger(JObject trigger, Type triggerParameterType = null)

0 commit comments

Comments
 (0)