Skip to content

Commit 38efb41

Browse files
committed
Console log buffer PR feedback and improvements:
- implement flush on dispose, so that logs are flushed when process shuts down - specify reader/writer usage of the channel - Change the write to console path to use WriteAsync instead of WaitToWriteAsync - Use TryComplete instead of Complete so that repeated completes do not throw - fix bug where a line could be read from buffer but not written to output
1 parent 977fdc8 commit 38efb41

File tree

3 files changed

+134
-29
lines changed

3 files changed

+134
-29
lines changed

src/WebJobs.Script.WebHost/Diagnostics/ConsoleWriter.cs

Lines changed: 44 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -3,24 +3,27 @@
33

44
using System;
55
using System.Text;
6+
using System.Threading;
67
using System.Threading.Channels;
78
using System.Threading.Tasks;
89

910
namespace Microsoft.Azure.WebJobs.Script.WebHost.Diagnostics
1011
{
11-
internal class ConsoleWriter
12+
internal class ConsoleWriter : IDisposable
1213
{
1314
// A typical out-of-proc function execution will generate 8 log lines.
1415
// A single core container can potentially get around 1K RPS at the higher end, and a typical log line is around 300 bytes
1516
// So in the extreme case, this is about 1 second of buffer and should be less than 3MB
1617
private const int DefaultBufferSize = 8000;
1718

19+
private static readonly TimeSpan DisposeTimeout = TimeSpan.FromSeconds(5);
1820
private static readonly TimeSpan DefaultConsoleBufferTimeout = TimeSpan.FromSeconds(1);
1921
private readonly Channel<string> _consoleBuffer;
2022
private readonly TimeSpan _consoleBufferTimeout;
2123
private readonly Action<Exception> _exceptionhandler;
2224
private Task _consoleBufferReadLoop;
2325
private Action<string> _writeEvent;
26+
private bool _disposed;
2427

2528
public ConsoleWriter(IEnvironment environment, Action<Exception> unhandledExceptionHandler)
2629
: this(environment, unhandledExceptionHandler, consoleBufferTimeout: DefaultConsoleBufferTimeout, autoStart: true)
@@ -36,10 +39,10 @@ internal ConsoleWriter(IEnvironment environment, Action<Exception> exceptionHand
3639
// We are going to used stdout, but do we write directly or use a buffer?
3740
_consoleBuffer = environment.GetEnvironmentVariable(EnvironmentSettingNames.ConsoleLoggingBufferSize) switch
3841
{
39-
"-1" => Channel.CreateUnbounded<string>(), // buffer size of -1 indicates that buffer should be enabled but unbounded
40-
"0" => null, // buffer size of 0 indicates that buffer should be disabled
42+
"-1" => Channel.CreateUnbounded<string>(new UnboundedChannelOptions() { SingleReader = true, SingleWriter = false }), // buffer size of -1 indicates that buffer should be enabled but unbounded
43+
"0" => null, // buffer size of 0 indicates that buffer should be disabled
4144
var s when int.TryParse(s, out int i) && i > 0 => Channel.CreateBounded<string>(i),
42-
_ => Channel.CreateBounded<string>(DefaultBufferSize), // default behavior is to use buffer with default size
45+
_ => Channel.CreateBounded<string>(new BoundedChannelOptions(DefaultBufferSize) { SingleReader = true, SingleWriter = false }), // default behavior is to use buffer with default size
4346
};
4447

4548
if (_consoleBuffer == null)
@@ -79,17 +82,20 @@ private void WriteToConsoleBuffer(string evt)
7982
{
8083
try
8184
{
82-
while (_consoleBuffer.Writer.TryWrite(evt) == false)
85+
if (_consoleBuffer.Writer.TryWrite(evt) == false)
8386
{
8487
// Buffer is currently full, wait until writing is permitted.
85-
// This is the downside of using channels, we are on a sync code path and so we have to block on this task
86-
var writeTask = _consoleBuffer.Writer.WaitToWriteAsync().AsTask();
87-
if (writeTask.WaitAsync(_consoleBufferTimeout).Result == false)
88+
using var source = new CancellationTokenSource(_consoleBufferTimeout);
89+
var writeTask = _consoleBuffer.Writer.WriteAsync(evt, source.Token);
90+
91+
// This is the downside of using channels, we are on a sync code path and so we have to block on this task if we want to wait for the buffer to clear.
92+
if (writeTask.IsCompleted)
8893
{
89-
// The buffer has been completed and does not allow further writes.
90-
// Currently this should not be possible, but the safest thing to do is just write directly to the console.
91-
Console.WriteLine(evt);
92-
break;
94+
writeTask.GetAwaiter().GetResult();
95+
}
96+
else
97+
{
98+
writeTask.AsTask().GetAwaiter().GetResult();
9399
}
94100
}
95101
}
@@ -133,7 +139,7 @@ private async Task ProcessConsoleBufferAsync(bool batched)
133139
{
134140
// if this has failed for any reason, fall everything back to console
135141
_writeEvent = Console.WriteLine;
136-
_consoleBuffer.Writer.Complete();
142+
_consoleBuffer.Writer.TryComplete();
137143
}
138144
}
139145

@@ -149,15 +155,8 @@ private async Task ProcessConsoleBufferBatchedAsync()
149155
{
150156
var builder = new StringBuilder();
151157

152-
while (true)
158+
while (await _consoleBuffer.Reader.WaitToReadAsync())
153159
{
154-
if (await _consoleBuffer.Reader.WaitToReadAsync() == false)
155-
{
156-
// The buffer has been completed and does not allow further reads.
157-
// Currently this should not be possible, but safest thing to do is break out of the loop.
158-
break;
159-
}
160-
161160
if (_consoleBuffer.Reader.TryRead(out string line1))
162161
{
163162
// Can we synchronously read multiple lines?
@@ -169,7 +168,7 @@ private async Task ProcessConsoleBufferBatchedAsync()
169168
builder.AppendLine(line2);
170169
int lines = 2;
171170

172-
while (_consoleBuffer.Reader.TryRead(out string nextLine) && lines < DefaultBufferSize)
171+
while (lines < DefaultBufferSize && _consoleBuffer.Reader.TryRead(out string nextLine))
173172
{
174173
builder.AppendLine(nextLine);
175174
lines++;
@@ -185,5 +184,28 @@ private async Task ProcessConsoleBufferBatchedAsync()
185184
}
186185
}
187186
}
187+
188+
protected virtual void Dispose(bool disposing)
189+
{
190+
if (!_disposed)
191+
{
192+
if (disposing)
193+
{
194+
if (_consoleBuffer != null)
195+
{
196+
_consoleBuffer.Writer.TryComplete();
197+
_consoleBufferReadLoop.Wait(DisposeTimeout);
198+
}
199+
}
200+
201+
_disposed = true;
202+
}
203+
}
204+
205+
public void Dispose()
206+
{
207+
Dispose(disposing: true);
208+
GC.SuppressFinalize(this);
209+
}
188210
}
189211
}

src/WebJobs.Script.WebHost/Diagnostics/LinuxContainerEventGenerator.cs

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,11 @@
22
// Licensed under the MIT License. See License.txt in the project root for license information.
33

44
using System;
5-
using System.Text;
6-
using System.Threading;
7-
using System.Threading.Channels;
8-
using System.Threading.Tasks;
95
using Microsoft.Extensions.Logging;
106

117
namespace Microsoft.Azure.WebJobs.Script.WebHost.Diagnostics
128
{
13-
internal class LinuxContainerEventGenerator : LinuxEventGenerator
9+
internal class LinuxContainerEventGenerator : LinuxEventGenerator, IDisposable
1410
{
1511
private const int MaxDetailsLength = 10000;
1612
private static readonly Lazy<LinuxContainerEventGenerator> _Lazy = new Lazy<LinuxContainerEventGenerator>(() => new LinuxContainerEventGenerator(SystemEnvironment.Instance, Console.WriteLine));
@@ -20,6 +16,7 @@ internal class LinuxContainerEventGenerator : LinuxEventGenerator
2016
private string _containerName;
2117
private string _stampName;
2218
private string _tenantId;
19+
private bool _disposed;
2320

2421
public LinuxContainerEventGenerator(IEnvironment environment, Action<string> writeEvent = null)
2522
{
@@ -152,5 +149,23 @@ public static void LogEvent(string message, Exception e = null, LogLevel logLeve
152149
slotName: SystemEnvironment.Instance.GetSlotName() ?? string.Empty,
153150
eventTimestamp: DateTime.UtcNow);
154151
}
152+
153+
protected virtual void Dispose(bool disposing)
154+
{
155+
if (!_disposed)
156+
{
157+
if (disposing)
158+
{
159+
_consoleWriter?.Dispose();
160+
}
161+
_disposed = true;
162+
}
163+
}
164+
165+
public void Dispose()
166+
{
167+
Dispose(disposing: true);
168+
GC.SuppressFinalize(this);
169+
}
155170
}
156171
}

test/WebJobs.Script.Tests/Diagnostics/LinuxContainerEventGeneratorWithConsoleOutputTests.cs

Lines changed: 70 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System;
55
using System.IO;
66
using System.IO.Abstractions.TestingHelpers;
7+
using System.Threading;
78
using System.Threading.Tasks;
89
using Microsoft.Azure.WebJobs.Script.WebHost.Diagnostics;
910
using Microsoft.Extensions.Logging;
@@ -153,7 +154,7 @@ public void MultipleEventsWithTinyBuffer_WritesDirectlyToConsoleOnTimeout(bool b
153154
// The first two messages are still stuck in the buffer. The third message will have been written to the console.
154155
// We should also have a log for the timeout exception that occurred while waiting for the buffer to become available.
155156
Assert.Equal(2, output.Length);
156-
Assert.StartsWith("MS_FUNCTION_LOGS 2,,,,,LogUnhandledException,\"System.AggregateException: One or more errors occurred. (The operation has timed out.)", output[0]);
157+
Assert.StartsWith("MS_FUNCTION_LOGS 2,,,,,LogUnhandledException,\"System.OperationCanceledException: The operation was canceled.", output[0]);
157158
Assert.Equal($"MS_FUNCTION_LOGS 4,C37E3412-86D1-4B93-BC5A-A2AE09D26C2D,TestApp,TestFunction3,TestEvent,TestSource,\"These are the details, lots of details\",\"This is the summary, a great summary\",{ScriptHost.Version},{timestamp.ToString("O")},TestExceptionType,\"Test exception message, with details\",E2D5A6ED-4CE3-4CFD-8878-FD4814F0A1F3,3AD41658-1C4E-4C9D-B0B9-24F2BDAE2829,F0AAA9AD-C3A6-48B9-A75E-57BB280EBB53,TEST-CONTAINER,test-stamp,test-tenant,TestRuntimeSiteName,TestSlotName", output[1]);
158159
}
159160

@@ -175,7 +176,7 @@ public async Task MultipleEventsBatchedWithTinyBuffer_BlocksOnWriteOnBufferFull_
175176
Assert.False(logTask.IsCompleted);
176177

177178
consoleWriter.StartProcessingBuffer(batched: true);
178-
await Task.Delay(TimeSpan.FromMilliseconds(10));
179+
await Task.Delay(TimeSpan.FromMilliseconds(50));
179180
Assert.True(logTask.IsCompleted);
180181

181182
using var sr = new StreamReader(_consoleOut);
@@ -188,5 +189,72 @@ public async Task MultipleEventsBatchedWithTinyBuffer_BlocksOnWriteOnBufferFull_
188189
Assert.Equal($"MS_FUNCTION_LOGS 4,C37E3412-86D1-4B93-BC5A-A2AE09D26C2D,TestApp,TestFunction2,TestEvent,TestSource,\"These are the details, lots of details\",\"This is the summary, a great summary\",{ScriptHost.Version},{timestamp.ToString("O")},TestExceptionType,\"Test exception message, with details\",E2D5A6ED-4CE3-4CFD-8878-FD4814F0A1F3,3AD41658-1C4E-4C9D-B0B9-24F2BDAE2829,F0AAA9AD-C3A6-48B9-A75E-57BB280EBB53,TEST-CONTAINER,test-stamp,test-tenant,TestRuntimeSiteName,TestSlotName", output[1]);
189190
Assert.Equal($"MS_FUNCTION_LOGS 4,C37E3412-86D1-4B93-BC5A-A2AE09D26C2D,TestApp,TestFunction3,TestEvent,TestSource,\"These are the details, lots of details\",\"This is the summary, a great summary\",{ScriptHost.Version},{timestamp.ToString("O")},TestExceptionType,\"Test exception message, with details\",E2D5A6ED-4CE3-4CFD-8878-FD4814F0A1F3,3AD41658-1C4E-4C9D-B0B9-24F2BDAE2829,F0AAA9AD-C3A6-48B9-A75E-57BB280EBB53,TEST-CONTAINER,test-stamp,test-tenant,TestRuntimeSiteName,TestSlotName", output[2]);
190191
}
192+
193+
[Fact]
194+
public async Task FlushesBufferOnDispose()
195+
{
196+
var env = CreateEnvironment(bufferSize: 10, batched: true);
197+
var consoleWriter = new ConsoleWriter(env, LinuxContainerEventGenerator.LogUnhandledException, consoleBufferTimeout: TimeSpan.FromMilliseconds(500), autoStart: true);
198+
var generator = new LinuxContainerEventGenerator(env, consoleWriter);
199+
200+
// Setup console output that will block until we release the semaphore.
201+
var controlledWriter = new ControlledWriter(_consoleOut);
202+
await controlledWriter.Semaphore.WaitAsync();
203+
Console.SetOut(controlledWriter);
204+
205+
var timestamp = DateTime.Parse("2023-04-19T14:12:00.0000000Z");
206+
generator.LogFunctionTraceEvent(LogLevel.Information, "C37E3412-86D1-4B93-BC5A-A2AE09D26C2D", "TestApp", "TestFunction1", "TestEvent", "TestSource", "These are the details, lots of details", "This is the summary, a great summary", "TestExceptionType", "Test exception message, with details", "E2D5A6ED-4CE3-4CFD-8878-FD4814F0A1F3", "3AD41658-1C4E-4C9D-B0B9-24F2BDAE2829", "F0AAA9AD-C3A6-48B9-A75E-57BB280EBB53", "TestRuntimeSiteName", "TestSlotName", timestamp);
207+
generator.LogFunctionTraceEvent(LogLevel.Information, "C37E3412-86D1-4B93-BC5A-A2AE09D26C2D", "TestApp", "TestFunction2", "TestEvent", "TestSource", "These are the details, lots of details", "This is the summary, a great summary", "TestExceptionType", "Test exception message, with details", "E2D5A6ED-4CE3-4CFD-8878-FD4814F0A1F3", "3AD41658-1C4E-4C9D-B0B9-24F2BDAE2829", "F0AAA9AD-C3A6-48B9-A75E-57BB280EBB53", "TestRuntimeSiteName", "TestSlotName", timestamp);
208+
await Task.Delay(TimeSpan.FromMilliseconds(20));
209+
210+
// We haven't released the semaphore yet, so nothing should have been written.
211+
Assert.Equal(0, _consoleOut.Length);
212+
213+
var disposeTask = Task.Run(consoleWriter.Dispose);
214+
await Task.Delay(TimeSpan.FromMilliseconds(20));
215+
216+
// Dispose should be blocked on flushing the buffer, which is blocked on the semaphore.
217+
Assert.False(disposeTask.IsCompleted);
218+
219+
controlledWriter.Semaphore.Release();
220+
await disposeTask;
221+
222+
// after being disposed, console writer should fall back to direct console write, just so that we don't get errors on logging paths
223+
generator.LogFunctionTraceEvent(LogLevel.Information, "C37E3412-86D1-4B93-BC5A-A2AE09D26C2D", "TestApp", "TestFunction3", "TestEvent", "TestSource", "These are the details, lots of details", "This is the summary, a great summary", "TestExceptionType", "Test exception message, with details", "E2D5A6ED-4CE3-4CFD-8878-FD4814F0A1F3", "3AD41658-1C4E-4C9D-B0B9-24F2BDAE2829", "F0AAA9AD-C3A6-48B9-A75E-57BB280EBB53", "TestRuntimeSiteName", "TestSlotName", timestamp);
224+
225+
using var sr = new StreamReader(_consoleOut);
226+
_consoleOut.Position = 0;
227+
var output = sr.ReadToEnd().Trim().SplitLines();
228+
229+
Assert.Equal(3, output.Length);
230+
231+
Assert.Equal($"MS_FUNCTION_LOGS 4,C37E3412-86D1-4B93-BC5A-A2AE09D26C2D,TestApp,TestFunction1,TestEvent,TestSource,\"These are the details, lots of details\",\"This is the summary, a great summary\",{ScriptHost.Version},{timestamp.ToString("O")},TestExceptionType,\"Test exception message, with details\",E2D5A6ED-4CE3-4CFD-8878-FD4814F0A1F3,3AD41658-1C4E-4C9D-B0B9-24F2BDAE2829,F0AAA9AD-C3A6-48B9-A75E-57BB280EBB53,TEST-CONTAINER,test-stamp,test-tenant,TestRuntimeSiteName,TestSlotName", output[0]);
232+
Assert.Equal($"MS_FUNCTION_LOGS 4,C37E3412-86D1-4B93-BC5A-A2AE09D26C2D,TestApp,TestFunction2,TestEvent,TestSource,\"These are the details, lots of details\",\"This is the summary, a great summary\",{ScriptHost.Version},{timestamp.ToString("O")},TestExceptionType,\"Test exception message, with details\",E2D5A6ED-4CE3-4CFD-8878-FD4814F0A1F3,3AD41658-1C4E-4C9D-B0B9-24F2BDAE2829,F0AAA9AD-C3A6-48B9-A75E-57BB280EBB53,TEST-CONTAINER,test-stamp,test-tenant,TestRuntimeSiteName,TestSlotName", output[1]);
233+
Assert.Equal($"MS_FUNCTION_LOGS 4,C37E3412-86D1-4B93-BC5A-A2AE09D26C2D,TestApp,TestFunction3,TestEvent,TestSource,\"These are the details, lots of details\",\"This is the summary, a great summary\",{ScriptHost.Version},{timestamp.ToString("O")},TestExceptionType,\"Test exception message, with details\",E2D5A6ED-4CE3-4CFD-8878-FD4814F0A1F3,3AD41658-1C4E-4C9D-B0B9-24F2BDAE2829,F0AAA9AD-C3A6-48B9-A75E-57BB280EBB53,TEST-CONTAINER,test-stamp,test-tenant,TestRuntimeSiteName,TestSlotName", output[2]);
234+
}
235+
236+
private class ControlledWriter : StreamWriter
237+
{
238+
public ControlledWriter(Stream stream) : base(stream)
239+
{
240+
this.AutoFlush = true;
241+
}
242+
243+
public SemaphoreSlim Semaphore { get; set; } = new SemaphoreSlim(1);
244+
245+
public override void Write(string value)
246+
{
247+
Semaphore.Wait();
248+
base.Write(value);
249+
Semaphore.Release();
250+
}
251+
252+
public override void WriteLine(string value)
253+
{
254+
Semaphore.Wait();
255+
base.WriteLine(value);
256+
Semaphore.Release();
257+
}
258+
}
191259
}
192260
}

0 commit comments

Comments
 (0)