55using System . Collections . Generic ;
66using System . Diagnostics ;
77using System . Globalization ;
8+ using System . IO ;
89using System . Linq ;
910using System . Text ;
1011using System . Threading ;
1112using System . Threading . Tasks ;
1213using Microsoft . Azure . WebJobs . Host ;
1314using Microsoft . Azure . WebJobs . Script . Config ;
1415using Microsoft . Azure . WebJobs . Script . Diagnostics ;
16+ using Microsoft . Azure . WebJobs . Script . IO ;
1517
1618namespace Microsoft . Azure . WebJobs . Script
1719{
@@ -37,10 +39,17 @@ public class ScriptHostManager : IDisposable
3739 private TraceWriter _traceWriter ;
3840
3941 private ScriptSettingsManager _settingsManager ;
42+ private AutoRecoveringFileSystemWatcher _scriptFileWatcher ;
43+ private CancellationTokenSource _restartDelayTokenSource ;
4044
4145 public ScriptHostManager ( ScriptHostConfiguration config )
4246 : this ( config , ScriptSettingsManager . Instance , new ScriptHostFactory ( ) )
4347 {
48+ if ( config . FileWatchingEnabled )
49+ {
50+ _scriptFileWatcher = new AutoRecoveringFileSystemWatcher ( config . RootScriptPath ) ;
51+ _scriptFileWatcher . Changed += OnScriptFileChanged ;
52+ }
4453 }
4554
4655 public ScriptHostManager ( ScriptHostConfiguration config , ScriptSettingsManager settingsManager , IScriptHostFactory scriptHostFactory )
@@ -83,8 +92,7 @@ public bool CanInvoke()
8392
8493 public void RunAndBlock ( CancellationToken cancellationToken = default ( CancellationToken ) )
8594 {
86- // Start the host and restart it if requested. Host Restarts will happen when
87- // host level configuration files change
95+ int consecutiveErrorCount = 0 ;
8896 do
8997 {
9098 ScriptHost newInstance = null ;
@@ -116,8 +124,8 @@ public bool CanInvoke()
116124
117125 if ( _traceWriter != null )
118126 {
119- string message = string . Format ( "Starting Host (HostId={0}, Version={1}, ProcessId={2}, Debug={3})" ,
120- newInstance . ScriptConfig . HostConfig . HostId , ScriptHost . Version , Process . GetCurrentProcess ( ) . Id , newInstance . InDebugMode . ToString ( ) ) ;
127+ string message = string . Format ( "Starting Host (HostId={0}, Version={1}, ProcessId={2}, Debug={3}, Attempt={4} )" ,
128+ newInstance . ScriptConfig . HostConfig . HostId , ScriptHost . Version , Process . GetCurrentProcess ( ) . Id , newInstance . InDebugMode . ToString ( ) , consecutiveErrorCount ) ;
121129 _traceWriter . Info ( message ) ;
122130 }
123131 newInstance . StartAsync ( cancellationToken ) . GetAwaiter ( ) . GetResult ( ) ;
@@ -131,6 +139,8 @@ public bool CanInvoke()
131139 // state to Running
132140 State = ScriptHostState . Running ;
133141 LastError = null ;
142+ consecutiveErrorCount = 0 ;
143+ _restartDelayTokenSource = null ;
134144
135145 // Wait for a restart signal. This event will automatically reset.
136146 // While we're restarting, it is possible for another restart to be
@@ -155,6 +165,7 @@ public bool CanInvoke()
155165 {
156166 State = ScriptHostState . Error ;
157167 LastError = ex ;
168+ consecutiveErrorCount ++ ;
158169
159170 // We need to keep the host running, so we catch and log any errors
160171 // then restart the host
@@ -177,15 +188,22 @@ public bool CanInvoke()
177188 } , TaskContinuationOptions . ExecuteSynchronously ) ;
178189 }
179190
180- // Wait for a short period of time before restarting to
181- // avoid cases where a host level config error might cause
182- // a rapid restart cycle
183- Task . Delay ( _config . RestartInterval ) . GetAwaiter ( ) . GetResult ( ) ;
191+ // attempt restarts using an exponential backoff strategy
192+ CreateRestartBackoffDelay ( consecutiveErrorCount ) . GetAwaiter ( ) . GetResult ( ) ;
184193 }
185194 }
186195 while ( ! _stopped && ! cancellationToken . IsCancellationRequested ) ;
187196 }
188197
198+ private Task CreateRestartBackoffDelay ( int consecutiveErrorCount )
199+ {
200+ _restartDelayTokenSource = new CancellationTokenSource ( ) ;
201+
202+ return Utility . DelayWithBackoffAsync ( consecutiveErrorCount , _restartDelayTokenSource . Token ,
203+ min : TimeSpan . FromSeconds ( 1 ) ,
204+ max : TimeSpan . FromMinutes ( 2 ) ) ;
205+ }
206+
189207 private static void LogErrors ( ScriptHost host )
190208 {
191209 if ( host . FunctionErrors . Count > 0 )
@@ -234,6 +252,19 @@ private async Task Orphan(ScriptHost instance, bool forceStop = false)
234252 }
235253 }
236254
255+ /// <summary>
256+ /// Handler for any file changes under the root script path
257+ /// </summary>
258+ private void OnScriptFileChanged ( object sender , FileSystemEventArgs e )
259+ {
260+ if ( _restartDelayTokenSource != null && ! _restartDelayTokenSource . IsCancellationRequested )
261+ {
262+ // if we're currently awaiting an error/restart delay
263+ // cancel that
264+ _restartDelayTokenSource . Cancel ( ) ;
265+ }
266+ }
267+
237268 public async Task StopAsync ( )
238269 {
239270 _stopped = true ;
@@ -326,6 +357,8 @@ protected virtual void Dispose(bool disposing)
326357 }
327358
328359 _stopEvent . Dispose ( ) ;
360+ _restartDelayTokenSource ? . Dispose ( ) ;
361+ _scriptFileWatcher ? . Dispose ( ) ;
329362
330363 _disposed = true ;
331364 }
0 commit comments