diff --git a/src/Microsoft.Azure.WebJobs.Host/Bindings/TraceWriter/TextWriterTraceAdapter.cs b/src/Microsoft.Azure.WebJobs.Host/Bindings/TraceWriter/TextWriterTraceAdapter.cs index 0ada1ae69..4ad5ab4ad 100644 --- a/src/Microsoft.Azure.WebJobs.Host/Bindings/TraceWriter/TextWriterTraceAdapter.cs +++ b/src/Microsoft.Azure.WebJobs.Host/Bindings/TraceWriter/TextWriterTraceAdapter.cs @@ -16,7 +16,7 @@ internal class TextWriterTraceAdapter : TextWriter private readonly StringBuilder _text; private readonly TraceWriter _traceWriter; - public TextWriterTraceAdapter(TraceWriter traceWriter) + private TextWriterTraceAdapter(TraceWriter traceWriter) : base(CultureInfo.InvariantCulture) { _text = new StringBuilder(); @@ -27,6 +27,10 @@ public override Encoding Encoding { get { return Encoding.Default; } } + public static TextWriter Synchronized(TraceWriter traceWriter) + { + return TextWriter.Synchronized(new TextWriterTraceAdapter(traceWriter)); + } public override void Write(char value) { @@ -34,7 +38,7 @@ public override void Write(char value) _text.Append(value); int len = _text.Length; - if (len > 2 && _text[len - 2] == '\r' && _text[len - 1] == '\n') + if (len >= 2 && _text[len - 2] == '\r' && _text[len - 1] == '\n') { // when we see a newline, flush the output // flushing often is very important - we need to ensure that output diff --git a/src/Microsoft.Azure.WebJobs.Host/Bindings/TraceWriter/TraceWriterBinding.cs b/src/Microsoft.Azure.WebJobs.Host/Bindings/TraceWriter/TraceWriterBinding.cs index 49eb7e943..7feebce7e 100644 --- a/src/Microsoft.Azure.WebJobs.Host/Bindings/TraceWriter/TraceWriterBinding.cs +++ b/src/Microsoft.Azure.WebJobs.Host/Bindings/TraceWriter/TraceWriterBinding.cs @@ -53,7 +53,7 @@ public Task BindAsync(BindingContext context) else { // bind to an adapter - tracer = new TextWriterTraceAdapter(context.Trace); + tracer = TextWriterTraceAdapter.Synchronized(context.Trace); } return BindAsync(tracer, context.ValueContext); diff --git a/src/Microsoft.Azure.WebJobs.Host/GlobalSuppressions.cs b/src/Microsoft.Azure.WebJobs.Host/GlobalSuppressions.cs index 2140e7e34..b850ca853 100644 --- a/src/Microsoft.Azure.WebJobs.Host/GlobalSuppressions.cs +++ b/src/Microsoft.Azure.WebJobs.Host/GlobalSuppressions.cs @@ -35,11 +35,13 @@ [assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1004:GenericMethodsShouldProvideTypeParameter", Scope = "member", Target = "Microsoft.Azure.WebJobs.Host.Bindings.IConverterManagerExtensions.#AddConverterBuilder`3(Microsoft.Azure.WebJobs.IConverterManager,System.Type,System.Object[])")] [assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1004:GenericMethodsShouldProvideTypeParameter", Scope = "member", Target = "Microsoft.Azure.WebJobs.Host.Bindings.IConverterManagerExtensions.#AddConverterBuilder`3(Microsoft.Azure.WebJobs.IConverterManager,System.Object)")] [assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Scope = "member", Target = "Microsoft.Azure.WebJobs.Host.Tables.TableAttributeBindingProvider+Table2IQueryableConverter`1.#Convert(Microsoft.Azure.WebJobs.Host.Storage.Table.IStorageTable)")] -[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Scope = "member", Target = "Microsoft.Azure.WebJobs.Host.Tables.TableAttributeBindingProvider+Object2ITableEntityConverter`1.#Convert(!0)")][assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA2204:Literals should be spelled correctly", MessageId = "WindowsAzure", Scope = "member", Target = "Microsoft.Azure.WebJobs.Host.AzureStorageDeploymentValidator.#Validate()")] +[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Scope = "member", Target = "Microsoft.Azure.WebJobs.Host.Tables.TableAttributeBindingProvider+Object2ITableEntityConverter`1.#Convert(!0)")] +[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA2204:Literals should be spelled correctly", MessageId = "WindowsAzure", Scope = "member", Target = "Microsoft.Azure.WebJobs.Host.AzureStorageDeploymentValidator.#Validate()")] [assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1062:Validate arguments of public methods", MessageId = "1", Scope = "member", Target = "Microsoft.Azure.WebJobs.Host.Bindings.AsyncCollectorBinding`2.#BuildAsync(!0,Microsoft.Azure.WebJobs.Host.Bindings.BindingContext)")] [assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1062:Validate arguments of public methods", MessageId = "1", Scope = "member", Target = "Microsoft.Azure.WebJobs.Host.Bindings.ExactTypeBindingProvider`2+ExactBinding.#BuildAsync(!0,Microsoft.Azure.WebJobs.Host.Bindings.BindingContext)")] [assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1062:Validate arguments of public methods", MessageId = "1", Scope = "member", Target = "Microsoft.Azure.WebJobs.Host.Bindings.GenericItemBindingProvider`1+Binding.#BuildAsync(!0,Microsoft.Azure.WebJobs.Host.Bindings.BindingContext)")] -[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1062:Validate arguments of public methods", MessageId = "1", Scope = "member", Target = "Microsoft.Azure.WebJobs.Host.Bindings.ItemBindingProvider`1+Binding.#BuildAsync(!0,Microsoft.Azure.WebJobs.Host.Bindings.BindingContext)")][assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Scope = "member", Target = "Microsoft.Azure.WebJobs.Host.Tables.TableAttributeBindingProvider+Object2ITableEntityConverter`1.#Convert(!0)")] +[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1062:Validate arguments of public methods", MessageId = "1", Scope = "member", Target = "Microsoft.Azure.WebJobs.Host.Bindings.ItemBindingProvider`1+Binding.#BuildAsync(!0,Microsoft.Azure.WebJobs.Host.Bindings.BindingContext)")] +[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Scope = "member", Target = "Microsoft.Azure.WebJobs.Host.Tables.TableAttributeBindingProvider+Object2ITableEntityConverter`1.#Convert(!0)")] [assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1004:GenericMethodsShouldProvideTypeParameter", Scope = "member", Target = "Microsoft.Azure.WebJobs.Host.Bindings.BindingFactory.#BindToInput`2(System.Boolean,System.Type,System.Object[])")] [assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1004:GenericMethodsShouldProvideTypeParameter", Scope = "member", Target = "Microsoft.Azure.WebJobs.Host.Bindings.BindingFactory.#BindToInput`2(System.Boolean,System.Object)")] [assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1004:GenericMethodsShouldProvideTypeParameter", Scope = "member", Target = "Microsoft.Azure.WebJobs.Host.Bindings.BindingFactory.#BindToInput`2(System.Object)")] @@ -49,4 +51,5 @@ [assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1004:GenericMethodsShouldProvideTypeParameter", Scope = "member", Target = "Microsoft.Azure.WebJobs.IConverterManager.#AddConverter`3(System.Func`3>)")] [assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1004:GenericMethodsShouldProvideTypeParameter", Scope = "member", Target = "Microsoft.Azure.WebJobs.IConverterManagerExtensions.#AddConverter`3(Microsoft.Azure.WebJobs.IConverterManager,System.Type,System.Object[])")] [assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1004:GenericMethodsShouldProvideTypeParameter", Scope = "member", Target = "Microsoft.Azure.WebJobs.IConverterManagerExtensions.#AddConverter`3(Microsoft.Azure.WebJobs.IConverterManager,System.Object)")] -[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1004:GenericMethodsShouldProvideTypeParameter", Scope = "member", Target = "Microsoft.Azure.WebJobs.Host.Bindings.BindingFactory.#BindToCollector`2(System.Type,System.Object[])")] \ No newline at end of file +[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1004:GenericMethodsShouldProvideTypeParameter", Scope = "member", Target = "Microsoft.Azure.WebJobs.Host.Bindings.BindingFactory.#BindToCollector`2(System.Type,System.Object[])")] +[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Scope = "member", Target = "Microsoft.Azure.WebJobs.Host.Bindings.TextWriterTraceAdapter.#Synchronized(Microsoft.Azure.WebJobs.Host.TraceWriter)")] \ No newline at end of file diff --git a/test/Microsoft.Azure.WebJobs.Host.UnitTests/Bindings/TraceWriter/TextWriterTraceAdapterTests.cs b/test/Microsoft.Azure.WebJobs.Host.UnitTests/Bindings/TraceWriter/TextWriterTraceAdapterTests.cs index 589bbd69d..a9cb8f851 100644 --- a/test/Microsoft.Azure.WebJobs.Host.UnitTests/Bindings/TraceWriter/TextWriterTraceAdapterTests.cs +++ b/test/Microsoft.Azure.WebJobs.Host.UnitTests/Bindings/TraceWriter/TextWriterTraceAdapterTests.cs @@ -1,9 +1,12 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the MIT License. See License.txt in the project root for license information. +using System.Collections.Generic; using System.Diagnostics; using System.IO; +using System.Threading.Tasks; using Microsoft.Azure.WebJobs.Host.Bindings; +using Microsoft.Azure.WebJobs.Host.TestCommon; using Moq; using Xunit; @@ -12,12 +15,12 @@ namespace Microsoft.Azure.WebJobs.Host.UnitTests.Bindings public class TextWriterTraceAdapterTests { private readonly Mock _mockTraceWriter; - private readonly TextWriterTraceAdapter _adapter; + private readonly TextWriter _adapter; public TextWriterTraceAdapterTests() { _mockTraceWriter = new Mock(MockBehavior.Strict, TraceLevel.Verbose); - _adapter = new TextWriterTraceAdapter(_mockTraceWriter.Object); + _adapter = TextWriterTraceAdapter.Synchronized(_mockTraceWriter.Object); } [Fact] @@ -70,5 +73,24 @@ public void Flush_FlushesRemainingBuffer() _mockTraceWriter.VerifyAll(); } + + [Fact] + public async Task TestMultipleThreads() + { + // This validates a bug where writing from multiple threads throws an exception. + TestTraceWriter trace = new TestTraceWriter(TraceLevel.Verbose); + TextWriter adapter = TextWriterTraceAdapter.Synchronized(trace); + + // Start Tasks to write + List tasks = new List(); + for (int i = 0; i < 1000; i++) + { + tasks.Add(adapter.WriteLineAsync(string.Empty)); + } + + await Task.WhenAll(tasks); + + Assert.Equal(1000, trace.Traces.Count); + } } }