Skip to content

Commit 6d4d558

Browse files
committed
Fixing concurrency issue when generating C# function target. Other minor enhancements to the invoker and generation process.
1 parent 2cbdd71 commit 6d4d558

File tree

6 files changed

+245
-92
lines changed

6 files changed

+245
-92
lines changed

src/WebJobs.Script/Description/CSharp/CSharpFunctionDescriptionProvider.cs

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ protected override Collection<ParameterDescriptor> GetFunctionParameters(IFuncti
8888

8989
try
9090
{
91-
MethodInfo functionTarget = csharpInvoker.GetFunctionTarget();
91+
MethodInfo functionTarget = csharpInvoker.GetFunctionTargetAsync().Result;
9292
ParameterInfo[] parameters = functionTarget.GetParameters();
9393
Collection<ParameterDescriptor> descriptors = new Collection<ParameterDescriptor>();
9494
IEnumerable<FunctionBinding> bindings = inputBindings.Union(outputBindings);
@@ -129,12 +129,20 @@ protected override Collection<ParameterDescriptor> GetFunctionParameters(IFuncti
129129

130130
return descriptors;
131131
}
132+
catch (AggregateException exc)
133+
{
134+
if (!(exc.InnerException is CompilationErrorException))
135+
{
136+
throw;
137+
}
138+
}
132139
catch (CompilationErrorException)
133140
{
134-
// We were unable to compile the function to get its signature,
135-
// setup the descriptor with the default parameters
136-
return base.GetFunctionParameters(functionInvoker, functionMetadata, triggerMetadata, methodAttributes, inputBindings, outputBindings);
137141
}
142+
143+
// We were unable to compile the function to get its signature,
144+
// setup the descriptor with the default parameters
145+
return base.GetFunctionParameters(functionInvoker, functionMetadata, triggerMetadata, methodAttributes, inputBindings, outputBindings);
138146
}
139147

140148
private ParameterDescriptor CreateTriggerParameterDescriptor(ParameterInfo parameter, BindingMetadata triggerMetadata,

src/WebJobs.Script/Description/CSharp/CSharpFunctionInvoker.cs

Lines changed: 106 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
using System.Linq;
1111
using System.Reflection;
1212
using System.Text;
13+
using System.Threading;
1314
using System.Threading.Tasks;
1415
using Microsoft.Azure.WebJobs.Host;
1516
using Microsoft.Azure.WebJobs.Script.Binding;
@@ -32,13 +33,14 @@ public class CSharpFunctionInvoker : ScriptFunctionInvokerBase
3233
private readonly Collection<FunctionBinding> _outputBindings;
3334
private readonly IFunctionEntryPointResolver _functionEntryPointResolver;
3435
private readonly IMetricsLogger _metrics;
36+
private readonly ReaderWriterLockSlim _functionValueLoaderLock = new ReaderWriterLockSlim();
3537

36-
private MethodInfo _function;
3738
private CSharpFunctionSignature _functionSignature;
3839
private FunctionMetadataResolver _metadataResolver;
3940
private Action _reloadScript;
4041
private Action _restorePackages;
41-
private Action<object[], object> _resultProcessor;
42+
private Action<MethodInfo, object[], object> _resultProcessor;
43+
private FunctionValueLoader _functionValueLoader;
4244

4345
private static readonly string[] WatchedFileTypes = { ".cs", ".csx", ".dll", ".exe" };
4446

@@ -59,6 +61,8 @@ internal CSharpFunctionInvoker(ScriptHost host, FunctionMetadata functionMetadat
5961
InitializeFileWatcherIfEnabled();
6062
_resultProcessor = CreateResultProcessor();
6163

64+
_functionValueLoader = FunctionValueLoader.Create(CreateFunctionTarget);
65+
6266
_reloadScript = ReloadScript;
6367
_reloadScript = _reloadScript.Debounce();
6468

@@ -97,41 +101,55 @@ protected override void OnScriptFileChanged(object sender, FileSystemEventArgs e
97101
private void ReloadScript()
98102
{
99103
// Reset cached function
100-
_function = null;
104+
ResetFunctionValue();
101105
TraceWriter.Verbose(string.Format(CultureInfo.InvariantCulture, "Script for function '{0}' changed. Reloading.", Metadata.Name));
102106

103107
TraceWriter.Verbose("Compiling function script.");
104-
var stopwatch = new Stopwatch();
105-
stopwatch.Start();
106108

107109
Script<object> script = CreateScript();
108110
Compilation compilation = script.GetCompilation();
109111
ImmutableArray<Diagnostic> compilationResult = compilation.GetDiagnostics();
110112

111-
stopwatch.Stop();
112-
113113
CSharpFunctionSignature signature = CSharpFunctionSignature.FromCompilation(compilation, _functionEntryPointResolver);
114114
compilationResult = ValidateFunctionBindingArguments(signature, compilationResult.ToBuilder());
115-
115+
116116
TraceCompilationDiagnostics(compilationResult);
117117

118118
bool compilationSucceeded = !compilationResult.Any(d => d.Severity == DiagnosticSeverity.Error);
119119

120-
TraceWriter.Verbose(string.Format(CultureInfo.InvariantCulture, "Compilation {0} ({1} ms).",
121-
compilationSucceeded ? "succeeded" : "failed", stopwatch.ElapsedMilliseconds));
120+
TraceWriter.Verbose(string.Format(CultureInfo.InvariantCulture, "Compilation {0}.",
121+
compilationSucceeded ? "succeeded" : "failed"));
122122

123123
// If the compilation succeeded, AND:
124124
// - We haven't cached a function (failed to compile on load), OR
125125
// - We're referencing local function types (i.e. POCOs defined in the function) AND Our our function signature has changed
126126
// Restart our host.
127-
if (compilationSucceeded &&
128-
(_functionSignature == null ||
127+
if (compilationSucceeded &&
128+
(_functionSignature == null ||
129129
(_functionSignature.HasLocalTypeReference || !_functionSignature.Equals(signature))))
130130
{
131131
_host.RestartEvent.Set();
132132
}
133133
}
134134

135+
private void ResetFunctionValue()
136+
{
137+
_functionValueLoaderLock.EnterWriteLock();
138+
try
139+
{
140+
if (_functionValueLoader != null)
141+
{
142+
_functionValueLoader.Dispose();
143+
}
144+
145+
_functionValueLoader = FunctionValueLoader.Create(CreateFunctionTarget);
146+
}
147+
finally
148+
{
149+
_functionValueLoaderLock.ExitWriteLock();
150+
}
151+
}
152+
135153
private void RestorePackages()
136154
{
137155
TraceWriter.Verbose("Restoring packages.");
@@ -146,7 +164,7 @@ private void RestorePackages()
146164
return;
147165
}
148166

149-
TraceWriter.Verbose("Packages restored.");
167+
TraceWriter.Verbose("Packages restored.");
150168
_reloadScript();
151169
});
152170
}
@@ -162,8 +180,8 @@ public override async Task Invoke(object[] parameters)
162180

163181
parameters = ProcessInputParameters(parameters);
164182

165-
MethodInfo function = GetFunctionTarget();
166-
183+
MethodInfo function = await GetFunctionTargetAsync();
184+
167185
object functionResult = function.Invoke(null, parameters);
168186

169187
if (functionResult is Task)
@@ -173,7 +191,7 @@ public override async Task Invoke(object[] parameters)
173191

174192
if (functionResult != null)
175193
{
176-
_resultProcessor(parameters, functionResult);
194+
_resultProcessor(function, parameters, functionResult);
177195
}
178196

179197
TraceWriter.Verbose("Function completed (Success)");
@@ -208,55 +226,82 @@ private object[] ProcessInputParameters(object[] parameters)
208226
return parameters;
209227
}
210228

211-
internal MethodInfo GetFunctionTarget()
229+
internal async Task<MethodInfo> GetFunctionTargetAsync(int attemptCount = 0)
212230
{
213-
if (_function == null)
231+
FunctionValueLoader currentValueLoader;
232+
_functionValueLoaderLock.EnterReadLock();
233+
try
234+
{
235+
currentValueLoader = _functionValueLoader;
236+
}
237+
finally
214238
{
215-
// TODO:Get this from some context set in/by the host.
216-
bool debug = true;
217-
MemoryStream assemblyStream = null;
218-
MemoryStream pdbStream = null;
239+
_functionValueLoaderLock.ExitReadLock();
240+
}
219241

220-
try
242+
try
243+
{
244+
return await currentValueLoader;
245+
}
246+
catch (OperationCanceledException)
247+
{
248+
// If the current task we were awaiting on was cancelled due to a
249+
// cache refresh, retry, which will use the new loader
250+
if (attemptCount > 2)
221251
{
222-
_assemblyLoader.ReleaseContext(Metadata);
252+
throw;
253+
}
254+
}
223255

224-
Script<object> script = CreateScript();
225-
Compilation compilation = GetScriptCompilation(script, debug);
226-
CSharpFunctionSignature functionSignature = CSharpFunctionSignature.FromCompilation(compilation, _functionEntryPointResolver);
256+
return await GetFunctionTargetAsync(++attemptCount);
257+
}
227258

228-
ValidateFunctionBindingArguments(functionSignature, throwIfFailed: true);
259+
private MethodInfo CreateFunctionTarget(CancellationToken cancellationToken)
260+
{
261+
// TODO:Get this from some context set in/by the host.
262+
bool debug = true;
263+
MemoryStream assemblyStream = null;
264+
MemoryStream pdbStream = null;
229265

230-
using (assemblyStream = new MemoryStream())
231-
{
232-
using (pdbStream = new MemoryStream())
233-
{
234-
var result = compilation.Emit(assemblyStream, pdbStream);
266+
try
267+
{
268+
Script<object> script = CreateScript();
269+
Compilation compilation = GetScriptCompilation(script, debug);
270+
CSharpFunctionSignature functionSignature = CSharpFunctionSignature.FromCompilation(compilation, _functionEntryPointResolver);
235271

236-
if (!result.Success)
237-
{
238-
throw new CompilationErrorException("Script compilation failed.", result.Diagnostics);
239-
}
272+
ValidateFunctionBindingArguments(functionSignature, throwIfFailed: true);
240273

241-
Assembly assembly = Assembly.Load(assemblyStream.GetBuffer(), pdbStream.GetBuffer());
242-
_assemblyLoader.CreateContext(Metadata, assembly, _metadataResolver);
274+
using (assemblyStream = new MemoryStream())
275+
{
276+
using (pdbStream = new MemoryStream())
277+
{
278+
var result = compilation.Emit(assemblyStream, pdbStream);
243279

244-
// Get our function entry point
245-
System.Reflection.TypeInfo scriptType = assembly.DefinedTypes.FirstOrDefault(t => string.Compare(t.Name, ScriptClassName, StringComparison.Ordinal) == 0);
246-
_function = _functionEntryPointResolver.GetFunctionEntryPoint(scriptType.DeclaredMethods.ToList());
247-
_functionSignature = functionSignature;
280+
// Check if cancellation was requested while we were compiling,
281+
// and if so quit here.
282+
cancellationToken.ThrowIfCancellationRequested();
283+
284+
if (!result.Success)
285+
{
286+
throw new CompilationErrorException("Script compilation failed.", result.Diagnostics);
248287
}
288+
289+
Assembly assembly = Assembly.Load(assemblyStream.GetBuffer(), pdbStream.GetBuffer());
290+
_assemblyLoader.CreateOrUpdateContext(Metadata, assembly, _metadataResolver);
291+
292+
// Get our function entry point
293+
System.Reflection.TypeInfo scriptType = assembly.DefinedTypes.FirstOrDefault(t => string.Compare(t.Name, ScriptClassName, StringComparison.Ordinal) == 0);
294+
_functionSignature = functionSignature;
295+
return _functionEntryPointResolver.GetFunctionEntryPoint(scriptType.DeclaredMethods.ToList());
249296
}
250297
}
251-
catch (CompilationErrorException ex)
252-
{
253-
TraceWriter.Error("Function compilation error");
254-
TraceCompilationDiagnostics(ex.Diagnostics);
255-
throw;
256-
}
257298
}
258-
259-
return _function;
299+
catch (CompilationErrorException ex)
300+
{
301+
TraceWriter.Error("Function compilation error");
302+
TraceCompilationDiagnostics(ex.Diagnostics);
303+
throw;
304+
}
260305
}
261306

262307
private void TraceCompilationDiagnostics(ImmutableArray<Diagnostic> diagnostics)
@@ -276,7 +321,7 @@ private ImmutableArray<Diagnostic> ValidateFunctionBindingArguments(CSharpFuncti
276321
if (!functionSignature.Parameters.Any(p => string.Compare(p.Name, _triggerInputName, StringComparison.Ordinal) == 0))
277322
{
278323
string message = string.Format(CultureInfo.InvariantCulture, "Missing a trigger argument named '{0}'.", _triggerInputName);
279-
var descriptor = new DiagnosticDescriptor(CSharpConstants.MissingTriggerArgumentCompilationCode,
324+
var descriptor = new DiagnosticDescriptor(CSharpConstants.MissingTriggerArgumentCompilationCode,
280325
"Missing trigger argument", message, "AzureFunctions", DiagnosticSeverity.Error, true);
281326

282327
resultBuilder.Add(Diagnostic.Create(descriptor, Location.None));
@@ -294,7 +339,7 @@ private ImmutableArray<Diagnostic> ValidateFunctionBindingArguments(CSharpFuncti
294339
if (!functionSignature.Parameters.Any(p => string.Compare(p.Name, binding.Name, StringComparison.Ordinal) == 0))
295340
{
296341
string message = string.Format(CultureInfo.InvariantCulture, "Missing binding argument named '{0}'.", binding.Name);
297-
var descriptor = new DiagnosticDescriptor(CSharpConstants.MissingBindingArgumentCompilationCode,
342+
var descriptor = new DiagnosticDescriptor(CSharpConstants.MissingBindingArgumentCompilationCode,
298343
"Missing binding argument", message, "AzureFunctions", DiagnosticSeverity.Warning, true);
299344

300345
resultBuilder.Add(Diagnostic.Create(descriptor, Location.None));
@@ -330,8 +375,9 @@ private Compilation GetScriptCompilation(Script<object> script, bool debug)
330375
.RemoveAllSyntaxTrees()
331376
.AddSyntaxTrees(scriptTree);
332377
}
333-
334-
return compilation.WithOptions(compilation.Options.WithOptimizationLevel(compilationOptimizationLevel));
378+
379+
return compilation.WithOptions(compilation.Options.WithOptimizationLevel(compilationOptimizationLevel))
380+
.WithAssemblyName(FunctionAssemblyLoader.GetAssemblyNameFromMetadata(Metadata, compilation.AssemblyName));
335381
}
336382

337383
private Script<object> CreateScript()
@@ -357,16 +403,16 @@ private static object GetTaskResult(Task task)
357403
return null;
358404
}
359405

360-
private Action<object[], object> CreateResultProcessor()
406+
private Action<MethodInfo, object[], object> CreateResultProcessor()
361407
{
362408
var bindings = _inputBindings.Union(_outputBindings).OfType<IResultProcessingBinding>();
363409

364-
Action<object[], object> processor = null;
410+
Action<MethodInfo, object[], object> processor = null;
365411
if (bindings.Any())
366412
{
367-
processor = (args, result) =>
413+
processor = (function, args, result) =>
368414
{
369-
ParameterInfo parameter = _function.GetParameters()
415+
ParameterInfo parameter = function.GetParameters()
370416
.FirstOrDefault(p => string.Compare(p.Name, _triggerInputName, StringComparison.Ordinal) == 0);
371417

372418
if (parameter != null)
@@ -383,7 +429,7 @@ private Action<object[], object> CreateResultProcessor()
383429
};
384430
}
385431

386-
return processor ?? ((_, __) => { /*noop*/ });
432+
return processor ?? ((_, __, ___) => { /*noop*/ });
387433
}
388434

389435
private static TraceLevel GetTraceLevelFromDiagnostic(Diagnostic diagnostic)

0 commit comments

Comments
 (0)