Skip to content

Commit f24f85c

Browse files
committed
Additional tracing and retries on compiler errors
1 parent 853d0e1 commit f24f85c

File tree

7 files changed

+132
-3
lines changed

7 files changed

+132
-3
lines changed
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
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.Linq;
7+
using System.Runtime.Serialization;
8+
using System.Text;
9+
using System.Threading.Tasks;
10+
using Microsoft.CodeAnalysis.Scripting;
11+
12+
namespace Microsoft.Azure.WebJobs.Script.Description
13+
{
14+
[Serializable]
15+
public sealed class CompilationServiceException : Exception
16+
{
17+
public CompilationServiceException()
18+
{
19+
}
20+
21+
public CompilationServiceException(string message) : base(message)
22+
{
23+
}
24+
25+
public CompilationServiceException(string message, Exception innerException) : base(message, innerException)
26+
{
27+
}
28+
29+
private CompilationServiceException(SerializationInfo info, StreamingContext context) : base(info, context)
30+
{
31+
}
32+
}
33+
}

src/WebJobs.Script/Description/DotNet/Compilation/CSharp/CSharpCompilation.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,8 +91,9 @@ private static bool IsOrUsesAssemblyType(ITypeSymbol typeSymbol, IAssemblySymbol
9191

9292
public Assembly Emit(CancellationToken cancellationToken)
9393
{
94-
using (var assemblyStream = new MemoryStream())
94+
try
9595
{
96+
using (var assemblyStream = new MemoryStream())
9697
using (var pdbStream = new MemoryStream())
9798
{
9899
var compilationWithAnalyzers = _compilation.WithAnalyzers(GetAnalyzers());
@@ -114,6 +115,10 @@ public Assembly Emit(CancellationToken cancellationToken)
114115
return Assembly.Load(assemblyStream.GetBuffer(), pdbStream.GetBuffer());
115116
}
116117
}
118+
catch (Exception exc) when (!(exc is CompilationErrorException))
119+
{
120+
throw new CompilationServiceException($"C# compilation service error: {exc.Message}", exc);
121+
}
117122
}
118123

119124
private static ImmutableArray<DiagnosticAnalyzer> GetAnalyzers()

src/WebJobs.Script/Description/DotNet/DotNetFunctionInvoker.cs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ public sealed class DotNetFunctionInvoker : FunctionInvokerBase
4040
private Action _restorePackages;
4141
private Action<MethodInfo, object[], object[], object> _resultProcessor;
4242
private string[] _watchedFileTypes;
43+
private int _compilerErrorCount;
4344

4445
internal DotNetFunctionInvoker(ScriptHost host,
4546
FunctionMetadata functionMetadata,
@@ -141,6 +142,7 @@ private async Task ReloadScriptAsync()
141142
{
142143
// Reset cached function
143144
_functionLoader.Reset();
145+
_compilerErrorCount = 0;
144146
TraceOnPrimaryHost(string.Format(CultureInfo.InvariantCulture, "Script for function '{0}' changed. Reloading.", Metadata.Name), TraceLevel.Info);
145147

146148
ImmutableArray<Diagnostic> compilationResult = ImmutableArray<Diagnostic>.Empty;
@@ -189,6 +191,24 @@ internal async Task<MethodInfo> GetFunctionTargetAsync()
189191
TraceCompilationDiagnostics(exc.Diagnostics, LogTargets.User);
190192
throw;
191193
}
194+
catch (CompilationServiceException exc)
195+
{
196+
const string message = "Compilation service error";
197+
TraceWriter.Error(message, exc, _compilationService.GetType().Name);
198+
Logger?.LogError(message);
199+
200+
// Compiler errors are often sporadic, so we'll attempt to reset the loader here to avoid
201+
// caching the compiler error and leaving the function hopelessly broken
202+
if (++_compilerErrorCount < 3)
203+
{
204+
_functionLoader.Reset();
205+
206+
const string resetMessage = "Function loader reset. Failed compilation result will not be cached.";
207+
TraceWriter.Info(resetMessage);
208+
Logger?.LogError(resetMessage);
209+
}
210+
throw;
211+
}
192212
}
193213

194214
private void RestorePackages()
@@ -305,6 +325,8 @@ private async Task<MethodInfo> CreateFunctionTarget(CancellationToken cancellati
305325
ImmutableArray<Diagnostic> bindingDiagnostics = ValidateFunctionBindingArguments(functionSignature, _triggerInputName, _inputBindings, _outputBindings, throwIfFailed: true);
306326
TraceCompilationDiagnostics(bindingDiagnostics);
307327

328+
_compilerErrorCount = 0;
329+
308330
// Set our function entry point signature
309331
_functionSignature = functionSignature;
310332

src/WebJobs.Script/Description/FunctionInvokerBase.cs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -233,9 +233,15 @@ protected virtual void OnScriptFileChanged(FileSystemEventArgs e)
233233
{
234234
}
235235

236-
protected void TraceOnPrimaryHost(string message, TraceLevel level)
236+
protected void TraceOnPrimaryHost(string message, TraceLevel level, string source = null, Exception exception = null)
237237
{
238-
TraceWriter.Trace(message, level, PrimaryHostTraceProperties);
238+
var traceEvent = new TraceEvent(level, message, source, exception);
239+
foreach (var item in PrimaryHostTraceProperties)
240+
{
241+
traceEvent.Properties.Add(item);
242+
}
243+
244+
TraceWriter.Trace(traceEvent);
239245
}
240246

241247
protected void PopulateExecutionContext(ExecutionContext context)

src/WebJobs.Script/GlobalSuppressions.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,4 +189,6 @@
189189
[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Scope = "member", Target = "Microsoft.Azure.WebJobs.Script.FunctionTraceWriterFactory.#CreateTraceWriter(Microsoft.Azure.WebJobs.Script.ScriptHostConfiguration,System.String)")]
190190
[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Scope = "member", Target = "Microsoft.Azure.WebJobs.Script.ScriptHost.#ConfigureLoggerFactory(Microsoft.Azure.WebJobs.Script.ScriptHostConfiguration,Microsoft.Azure.WebJobs.Script.IFunctionTraceWriterFactory,Microsoft.Azure.WebJobs.Script.Config.ScriptSettingsManager,System.Func`1<System.Boolean>)")]
191191
[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Scope = "member", Target = "Microsoft.Azure.WebJobs.Script.DefaultLoggerFactoryBuilder.#AddLoggerProviders(Microsoft.Extensions.Logging.ILoggerFactory,Microsoft.Azure.WebJobs.Script.ScriptHostConfiguration,Microsoft.Azure.WebJobs.Script.Config.ScriptSettingsManager)")]
192+
[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Scope = "member", Target = "Microsoft.Azure.WebJobs.Script.Description.CSharpCompilation.#Emit(System.Threading.CancellationToken)")]
193+
[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Scope = "member", Target = "Microsoft.Azure.WebJobs.Script.DefaultLoggerFactoryBuilder.#AddLoggerProviders(Microsoft.Extensions.Logging.ILoggerFactory,Microsoft.Azure.WebJobs.Script.ScriptHostConfiguration,Microsoft.Azure.WebJobs.Script.Config.ScriptSettingsManager)")]
192194
[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Sku", Scope = "member", Target = "Microsoft.Azure.WebJobs.Script.ScriptConstants.#DynamicSkuConnectionLimit")]

src/WebJobs.Script/WebJobs.Script.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -415,6 +415,7 @@
415415
<Compile Include="Binding\SimpleValueProvider.cs" />
416416
<Compile Include="Config\FeatureFlags.cs" />
417417
<Compile Include="Description\Binding\Cardinality.cs" />
418+
<Compile Include="Description\Compilation\CompilationServiceException.cs" />
418419
<Compile Include="Description\Compilation\ICompilation.cs" />
419420
<Compile Include="Description\DiagnosticSeverityExtensions.cs" />
420421
<Compile Include="Description\DotNet\Compilation\CSharp\Analyzers\AsyncVoidAnalyzer.cs" />

test/WebJobs.Script.Tests/Description/DotNet/DotNetFunctionInvokerTests.cs

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,66 @@ public void ValidateFunctionBindingArguments_ReturnBinding_Succeeds()
192192
Assert.Equal(0, diagnostics.Count());
193193
}
194194

195+
[Fact]
196+
public async Task CompilerError_IsRetried_UpToLimit()
197+
{
198+
// Create the invoker dependencies and setup the appropriate method to throw the exception
199+
RunDependencies dependencies = CreateDependencies();
200+
201+
// Set the host to primary
202+
dependencies.Host.SetupGet(h => h.IsPrimary).Returns(true);
203+
204+
var metadata = new FunctionMetadata
205+
{
206+
ScriptFile = "run.csx",
207+
FunctionDirectory = "c:\\somedir",
208+
Name = Guid.NewGuid().ToString(),
209+
ScriptType = ScriptType.CSharp
210+
};
211+
212+
metadata.Bindings.Add(new BindingMetadata() { Name = "myQueueItem", Type = "ManualTrigger" });
213+
214+
var testBinding = new Mock<FunctionBinding>(null, new BindingMetadata() { Name = "TestBinding", Type = "blob" }, FileAccess.Write);
215+
216+
var dotNetCompilation = new Mock<IDotNetCompilation>();
217+
var dotnetCompilationService = new Mock<ICompilationService<IDotNetCompilation>>();
218+
dotnetCompilationService.SetupSequence(s => s.GetFunctionCompilationAsync(It.IsAny<FunctionMetadata>()))
219+
.ThrowsAsync(new CompilationServiceException("1"))
220+
.ThrowsAsync(new CompilationServiceException("2"))
221+
.ThrowsAsync(new CompilationServiceException("3"))
222+
.ThrowsAsync(new CompilationServiceException("4")); // This should not be reached
223+
224+
var compilationFactory = new Mock<ICompilationServiceFactory<ICompilationService<IDotNetCompilation>, IFunctionMetadataResolver>>();
225+
compilationFactory.Setup(f => f.CreateService(ScriptType.CSharp, It.IsAny<IFunctionMetadataResolver>())).Returns(dotnetCompilationService.Object);
226+
227+
var invoker = new DotNetFunctionInvoker(dependencies.Host.Object, metadata, new Collection<FunctionBinding>(),
228+
new Collection<FunctionBinding> { testBinding.Object }, new FunctionEntryPointResolver(), new FunctionAssemblyLoader(string.Empty),
229+
compilationFactory.Object, new Mock<IFunctionMetadataResolver>().Object);
230+
231+
var arguments = new object[]
232+
{
233+
new ExecutionContext()
234+
{
235+
FunctionDirectory = "c:\\test",
236+
FunctionName = "test",
237+
InvocationId = Guid.NewGuid()
238+
}
239+
};
240+
241+
for (int i = 1; i <= 10; i++)
242+
{
243+
var expectedAttempt = Math.Min(i, 3);
244+
CompilationServiceException exception = await Assert.ThrowsAsync<CompilationServiceException>(() => invoker.Invoke(arguments));
245+
Assert.Equal(expectedAttempt.ToString(), exception.Message);
246+
}
247+
248+
var compilerErrorTraces = dependencies.TraceWriter.Traces
249+
.Where(t => string.Equals(t.Message, "Function loader reset. Failed compilation result will not be cached."));
250+
251+
// 3 attempts total, make sure we've logged the 2 retries.
252+
Assert.Equal(2, compilerErrorTraces.Count());
253+
}
254+
195255
[Theory]
196256
[InlineData(false, true, true)]
197257
[InlineData(false, false, false)]

0 commit comments

Comments
 (0)